Search code examples
reactjstypescriptreact-router-dom

How to pass elements to other page layout sections from inside a nested route?


I have a page layout with 3 separate sections: The sidebar, the header and the main body. I would like to fill the main body from the child route, while parent routes define the header or sidebar - but after trying to work with Providers and Context approaches, I couldn't quite find the right approach.

Here's a baseline / pseudo-code of what I would like to achieve:

Example Layout:

layout.tsx

export function DefaultLayout() {
  return (
    <div className="sidebar">
      <!-- sidebar content goes here -->
    </div>
    <div className="page">
      <div className="page-header">
        <!-- header content goes here -->
      </div>
      <div className="page-body">
        <Outlet />
      </div>
    </div>
  );
}

Example routes:

router

import { Route } from "react-router-dom";

<Route element={<DefaultLayout />}>
  <Route
    path="/dashboard"
    sidebar={<DefaultSidebar />}
    header={<DashboardHeader />}
  >
    <Route path="home" element={<DashboardHomePage />}>
    <Route path="stats" element={<DashboardStatsPage />}>
    <Route path="help" element={<DashboardHelpPage />}>
  </Route>
  <Route path="/user" sidebar={<UserSidebar />} header={<UserHeader />}>
    <Route path="overview" element={<UserOverviewPage />}>
    <Route path="accounts" element={<UserAccountsPage />}>
  </Route>
</Route>

As seen in the simplified example above, I would like to define the overall Layout in a parent route, while "filling" the layout sections in nested routes. sidebar={} and header={} are just pseudo-code for the example, I realize that it's probably part of another element={} wrapper.

I've tried solving this by creating a <SidebarProvider> and <HeaderProvider> component, working with things like useOutletContext and passing the state downwards, but that didn't seem to work and was quickly getting hopelessly complicated (infinite loops, etc).

So, starting fresh: what would be a method to achieve the above behavior? If it's via providers, how should I structure them?

Edit: To clarify confusions: I want to decouple the Layout, the sidebar, the header and the routes (which hold the body). I think this should be possible with Providers, as I found examples of using Providers for a similar situation, just to set a page title which don't seem to work when I try to extend them with multiple elements. What would a Provider example, that takes a sidebar and a header, look like?

Essentially: I want to pass 2 elements from a child route, to a parent route. How do I do that?


Solution

  • I would suggest adding the header and sidebar as props to the DefaultLayout route component and render them accordingly. Don't forget that in React we use a className prop instead of class.

    const DefaultLayout = ({ header, sidebar }) => (
      <div className="layout">
        <div className="sidebar">{sidebar}</div>
        <div className="page">
          <div className="page-header">{header}</div>
          <div className="page-body">
            <Outlet />
          </div>
        </div>
      </div>
    );
    

    Note that I added a "layout" class to the overall surrounding element of the entire element in order to set the overall page layout using a grid layout and assigned areas.

    Example CSS:

    .layout {
      display: grid;
      grid-template-areas: "sidebar page";
    }
    
    .sidebar {
      grid-area: sidebar;
    }
    
    .page {
      grid-area: page;
    }
    

    You will then adjust the routes to pass the sidebar and header components to the layout route each.

    <Routes>
      <Route
        element={
          <DefaultLayout
            sidebar={<DefaultSidebar />}
            header={<DashboardHeader />}
          />
        }
      >
        <Route path="/dashboard">
          <Route path="home" element={<DashboardHomePage />} />
          <Route path="stats" element={<DashboardStatsPage />} />
          <Route path="help" element={<DashboardHelpPage />} />
        </Route>
      </Route>
      <Route
        element={
          <DefaultLayout sidebar={<UserSidebar />} header={<UserHeader />} />
        }
      >
        <Route path="/user">
          <Route path="overview" element={<UserOverviewPage />} />
          <Route path="accounts" element={<UserAccountsPage />} />
        </Route>
      </Route>
    </Routes>
    

    Edit how-to-pass-elements-to-other-page-layout-sections-from-inside-a-nested-route

    enter image description here enter image description here

    Slightly more DRY solution

    You can refactor the DefaultLayout to split the outer-most div from the inner sidebar and page div elements.

    const DefaultLayout = () => (
      <div className="layout">
        <Outlet />
      </div>
    );
    
    const SidebarHeaderLayout = ({ header, sidebar }) => (
      <>
        <div className="sidebar">{sidebar}</div>
        <div className="page">
          <div className="page-header">{header}</div>
          <div className="page-body">
            <Outlet />
          </div>
        </div>
      </>
    );
    
    <Routes>
      <Route element={<DefaultLayout />}>
        <Route
          path="/dashboard"
          element={
            <SidebarHeaderLayout
              sidebar={<DefaultSidebar />}
              header={<DashboardHeader />}
            />
          }
        >
          <Route path="home" element={<DashboardHomePage />} />
          <Route path="stats" element={<DashboardStatsPage />} />
          <Route path="help" element={<DashboardHelpPage />} />
        </Route>
        <Route
          path="/user"
          element={
            <SidebarHeaderLayout
              sidebar={<UserSidebar />}
              header={<UserHeader />}
            />
          }
        >
          <Route path="overview" element={<UserOverviewPage />} />
          <Route path="accounts" element={<UserAccountsPage />} />
        </Route>
      </Route>
    </Routes>
    

    Edit how-to-pass-elements-to-other-page-layout-sections-from-inside-a-nested-route (forked)