javascriptreactjsreact-router-domoutlet

How to use multiple Outlet


I can't get the info if it is possible or not to have "multiple nested" Outlet in React with router dom. I have some protected routes created using createBrowserRouter:

const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: (
          <RequireAuth allowedRoles={[ROLES.User, ROLES.Editor, ROLES.Admin]}>
            <Activities />
          </RequireAuth>
        ),
        loader: activitiesLoader,
      },
      {
        path: "new-activity",
        element: (
          <RequireAuth allowedRoles={[ROLES.Admin]}>
            <NewActivity />
          </RequireAuth>
        ),
        action: newActivityAction
      },
      {
        path: "register",
        element: <Register />
      },
      {
        path: "login",
        element: <Login />
      },
    ]
  },
]);

The Layout component is quite simple with an Outlet:

export default function Layout() {
  return (
    <>
      <div id="nav">
        <Nav />
      </div>
      <div id="content">
        <Outlet />
      </div>
    </>
  );
}

But as the root route is protected, it goes through a RequireAuth middleware that has an Outlet as well:

const RequireAuth = ({ allowedRoles }) => {
  const { auth } = useAuth();
  const location = useLocation();

  return (
    allowedRoles?.includes(auth?.role)
      ? <Outlet />
      : auth?.email
        ? <Navigate to="/unauthorized" state={{ from: location }} replace />
        : <Navigate to="/login" state={{ from: location }} replace />
  );
};

The auth works well, but only the Layout is rendered, the "second" Outlet doesn't render the child elements. Is it normal, I am not supposed to use 2 Outlet that way?


Solution

  • You can certainly use multiple nested Outlet components, one at each level of nesting, providing an outlet for the nested routes under it. You are using RequireAuth like a wrapper component instead of a layout route component.

    Refactor the code to render RequireAuth as layout routes and move the route content to nested routes under it.

    Example:

    const router = createBrowserRouter([
      {
        path: "/",
        element: <Layout />,
        errorElement: <ErrorPage />,
        children: [
          // rendered into Layout's Outlet
          {
            element: (
              <RequireAuth
                allowedRoles={[ROLES.User, ROLES.Editor, ROLES.Admin]}
              />
            ),
            loader: activitiesLoader,
            children: [
              // rendered into RequireAuth Outlet
              { index: true, element: <Activities /> },
            ],
          },
          {
            element: <RequireAuth allowedRoles={[ROLES.Admin]} />,
            action: newActivityAction,
            children: [
              // rendered into RequireAuth Outlet
              { path: "new-activity", element: <NewActivity /> },
            ],
          },
          { path: "register", element: <Register /> },
          { path: "login", element: <Login /> },
        ]
      },
    ]);
    

    Alternatively if you wanted to keep RequireAuth as a wrapper component then it should consume and render the children prop in place of the Outlet.

    Example:

    const RequireAuth = ({ allowedRoles, children }) => {
      const { auth } = useAuth();
      const location = useLocation();
    
      return allowedRoles?.includes(auth?.role)
        ? children
        : (
          <Navigate
            to={auth?.email ? "/unauthorized" : "/login"}
            state={{ from: location }}
            replace
          />
        );
    };