Search code examples
reactjstypescriptreact-routerreact-router-dom

Shared routes in different layouts react


I have a React app with two layouts: GuestLayout and AuthLayout. Each of one renders a Navbar, and Outlet and a Footer depending if the user is logged:

AuthLayout.tsx

interface Props {
  isAllowed: boolean
  redirectPath: string
  children: JSX.Element
}

export const AuthLayout = ({ isAllowed, redirectPath, children }: Props) => {
  const location = useLocation()

  if (!isAllowed) {
    return <Navigate to={redirectPath} replace state={{ from: location }} />
  }

  return (
    <div className='flex flex-col h-screen'>
      <Navbar />
      <main className='flex-1 min-h-max p-8 bg-slate-100 dark:bg-neutral-800'>
        {children || <Outlet />}
      </main>
      <Footer />
    </div>
  )
}

GuestLayout.tsx

export const GuestLayout = () => (
  <div className='flex flex-col h-screen'>
    <GuestNavbar />
    <main className='flex-1 min-h-max p-8 bg-slate-100 dark:bg-neutral-800'>
      <Outlet />
    </main>
    <Footer />
  </div>
)

The Footer(used in both layouts), display a series of links that can be visited either the user is logged or not, and I don't know hot to do it properly. Here is my router:

app.tsx

<Route element={<GuestLayout />}>
  <Route index element={<Login />} />
  <Route path='login' element={<Login />} />
  <Route path='recover-password' element={<RecoverPassword />} />
  <Route path='legal' element={<Outlet />}>
    <Route path='privacy' element={<LegalPageComponent page='privacyPolicy' />} />
    <Route path='notice' element={<LegalPageComponent page='legalNotice' />} />
  </Route>
</Route>

<Route
  element={<AuthLayout isAllowed={!!user} redirectPath='/login' children={<Outlet />} />}
>
  <Route index element={<Home />} />
  <Route path='home' element={<Home />} />
  <Route
    path='processes'
    element={
      <Suspense fallback={<>...</>}>
        <ProcessManagement />
      </Suspense>
    }
  />
  <Route path='management' element={<Outlet />}>
    <Route index element={<Management />} />
    <Route path='clients' element={<ClientManagement />} />
    <Route path='projects' element={<ProjectManagement />} />
  </Route>
  <Route path='legal' element={<Outlet />}>
    <Route path='privacy' element={<LegalPageComponent page='privacyPolicy' />} />
    <Route path='notice' element={<LegalPageComponent page='legalNotice' />} />
  </Route>
</Route>

As you can see, I need the legal pages to be displayed in both layouts, but I don't know how to do it. If I do this, it goes always to GuestLayout.

Is this approach correct?


Solution

  • You can unnest the "/legal" layout route and render the nested routes in the a new layout that conditionally renders the guest or auth UI.

    It looks like the "/" index route rendering either Login or Home has the same issue. Render Login on only the "/login" path, and render a "catch-all" path="*" route to redirect users to "/home". If the user is not authenticated then the AuthLayout will redirect them to "/login".

    Example:

    interface Props {
      isAllowed: boolean;
    }
    
    export const CommonLayout = ({ isAllowed }: Props) => {
      return (
        <div className='flex flex-col h-screen'>
          {isAllowed ? <Navbar /> : <GuestNavbar />}
          <main className='flex-1 min-h-max p-8 bg-slate-100 dark:bg-neutral-800'>
            <Outlet />
          </main>
          <Footer />
        </div>;
      );
    };
    
    <Routes>
      <Route element={<GuestLayout />}>
        <Route path='login' element={<Login />} />
        <Route path='recover-password' element={<RecoverPassword />} />
      </Route>
    
      <Route
        element={<AuthLayout isAllowed={!!user} redirectPath='/login' />}
      >
        <Route path='home' element={<Home />} />
        <Route
          path='processes'
          element={
            <Suspense fallback={<>...</>}>
              <ProcessManagement />
            </Suspense>
          }
        />
        <Route path='management'>
          <Route index element={<Management />} />
          <Route path='clients' element={<ClientManagement />} />
          <Route path='projects' element={<ProjectManagement />} />
        </Route>
      </Route>
    
      <Route element={<CommonLayout />}>
        <Route path='legal'>
          <Route
            path='privacy'
            element={<LegalPageComponent page='privacyPolicy' />}
          />
          <Route
            path='notice'
            element={<LegalPageComponent page='legalNotice' />}
          />
        </Route>
      </Route>
    
      <Route path="*" element={<Navigate to="/home" replace />} />
    </Routes>
    

    Since the guest and auth layouts really only differ in which header they render, UI-wise, it may be an improvement to instead render all routes into the CommonLayout to get the conditional rendering of one header or the other, and remove the UI/visual markup from AuthLayout so it's only handling the auth logic.

    Example:

    interface Props {
      isAllowed: boolean;
    }
    
    export const CommonLayout = ({ isAllowed }: Props) => {
      return (
        <div className='flex flex-col h-screen'>
          {isAllowed ? <Navbar /> : <GuestNavbar />}
          <main className='flex-1 min-h-max p-8 bg-slate-100 dark:bg-neutral-800'>
            <Outlet />
          </main>
          <Footer />
        </div>;
      );
    };
    
    interface Props {
      isAllowed: boolean;
      redirectPath: string;
    }
    
    export const AuthLayout = ({ isAllowed, redirectPath }: Props) => {
      const location = useLocation()
    
      return isAllowed
        ? <Outlet />
        : (
          <Navigate
            to={redirectPath}
            replace
            state={{ from: location }}
          />
        );
    };
    
    <Routes>
      <Route element={<CommonLayout />}>
        <Route path='login' element={<Login />} />
        <Route path='recover-password' element={<RecoverPassword />} />
    
        <Route path='legal'>
          <Route
            path='privacy'
            element={<LegalPageComponent page='privacyPolicy' />}
          />
          <Route
            path='notice'
            element={<LegalPageComponent page='legalNotice' />}
          />
        </Route>
    
        <Route
          element={<AuthLayout isAllowed={!!user} redirectPath='/login' />}
        >
          <Route path='home' element={<Home />} />
          <Route
            path='processes'
            element={
              <Suspense fallback={<>...</>}>
                <ProcessManagement />
              </Suspense>
            }
          />
          <Route path='management'>
            <Route index element={<Management />} />
            <Route path='clients' element={<ClientManagement />} />
            <Route path='projects' element={<ProjectManagement />} />
          </Route>
        </Route>
      </Route>
      <Route path="*" element={<Navigate to="/home" replace />} />
    </Routes>