5

I am learning ReactJS so I am completely new at this.

I created an authentication/authorization service that I know works in React just fine. What I am trying to do is protect the primary application with a login. As I understand, I want a high-order component such as this in order to protect the core of the application:

const withAuth = (Component) => {
  const AuthenticatedComponent = () => {
    const isAuthenticated = MyService.isUserAuthenticated();
    if (!isAuthenticated) {
      return <Navigate to="/login" />;
    }
    return <Component />;
  };

  return AuthenticatedComponent;
};

export default withAuth;

I have two components that are pretty simple to test the theory that are both very similar. Essentially they look like this:

export default function DefaultLandingPage() {
  return(
    <>
    <div>
      This would be the dashboard page after login.
    </div>
    </>
  );
}
export default function LoginPage() {
  return(
    <>
    <div>
      This is where the user would log in.
    </div>
    </>
  );
}

I tried to use these in my App.js file.

First I tried this:

function App() {
  return (
    <>
    <Routes>
      <Route path="/" exact component={withAuth(DefaultLandingPage) } />
      <Route path="/login" element={ <LoginPage /> } />
    </Routes>
    </>
  );
}

That results in:

Matched leaf route at location "/" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.

I tried changing it to:

<Route path="/" exact element={ withAuth(DefaultLandingPage) } />

Which results in:

Warning: Functions are not valid as a React child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than return it.

Can someone point me in the proper direction?

I did find this post and am headed down that route right now; however, I am not sure what my incorrect logic is here.

Can someone explain why my logic doesn't work here?

What concerns me, is that I want to also protect the pages with fine-grained privileges and was hoping to create something that would function like this:

<Route path="/" exact element={ withAuth(DefaultLandingPage) } />
<Route path="/some-sub-area" element={ withAuth(withPermission(SomeSubArea, 'permission-xyz')) } />
el n00b
  • 1,957
  • 7
  • 37
  • 64

2 Answers2

0

For your first case, make sure your prop component is capitalized to Component per the router documentation. For your second issue, your withAuth function returns a function reference to AuthenticatedComponent rather than treating it as a functional component. Opting to return <AuthenticatedComponent/> should send you in the right direction!

Nicholas Dullam
  • 291
  • 1
  • 9
  • Those two things make sense. Is the preferred configuration as of v6 or should I head full throttle down the v6 route listed on that other post? – el n00b May 07 '23 at 00:14
  • 1
    The v6 route listed on the post (https://stackoverflow.com/a/70954599/12109958) is the most standard configuration for private/protected routes -- although if you just remove your `withAuth` wrapper, and render the components children (i.e.``) in `AuthenticatedComponent` you're already there : ) – Nicholas Dullam May 07 '23 at 00:20
  • I'll head down that route. I have just a blank app isolated to learn nothing but this, so it's not like I'm throwing away a ton of work. – el n00b May 07 '23 at 00:23
  • Nice, good luck! – Nicholas Dullam May 07 '23 at 00:27
  • FYI - got it working. Now to figure out how to protect it as a SPA. – el n00b May 07 '23 at 13:37
0

The react-router-dom Route component hasn't any component, so this is the cause of the first error regarding a missing element.

The Route component's element prop takes only a React.ReactNode, e.g. it takes a JSX literal. withAuth(DefaultLandingPage) isn't valid JSX.

You've a couple options:

  1. Export a decorated version of the DefaultLandingPage component, to be rendered as JSX.

    const DefaultLandingPage = () => {
      ...
    };
    
    export default withAuth(DefaultLandingPage);
    
    function App() {
      return (
        <Routes>
          <Route path="/" element={<DefaultLandingPage /> } />
          <Route path="/login" element={<LoginPage />} />
        </Routes>
      );
    }
    
  2. Implement protected routes in a more conventional method, as per the Stackoverflow answer you linked. Using Higher Order Components for authentication as a little old these days.

    const AuthenticatedComponent = () => {
      const isAuthenticated = MyService.isUserAuthenticated();
    
      return isAuthenticated
        ? <Outlet />
        : <Navigate to="/login" replace />;
    };
    
    function App() {
      return (
        <Routes>
          <Route element={<AuthenticatedComponent />}>
            <Route path="/" element={<DefaultLandingPage /> } />
          </Route>
          <Route path="/login" element={<LoginPage />} />
        </Routes>
      );
    }
    

If the eventual goal is to also use role-based route protection then you'd implement something similar to the following:

const PermissionRoute = ({ permissions = [] }) => {
  const permission = /* wherever you get current user's permission */

  return !permissions.length || permissions.includes(permission) ? (
    <Outlet />
  ) : (
    <Navigate to="/login" replace /> // <-- or any safe non-permission route
  );
};
function App() {
  return (
    <Routes>
      ...
      <Route element={<PermissionRoute permissions={["...", ...]} />}>
        ...
      </Route>
      ...
    </Routes>
  );
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181