← Go Back

React Router and nested routes

Following the public launch of Remix v1, nested routes have become all the rage - at least within my tiny corner of cyberspace. But what are nested routes, why are they meaningful, and how can you use nested routes in your React app?

React Router

Nested routes have existed in React Router since very early on - heck, it was initially named react-nested-router. Now on version 6, React Router is one of the most popular React packages and will be used throughout this post to demonstrate the concept of nested routes. I will also include React Router v5 code samples and demos but, I will refer to the v6 version when explaining the code.

Nested routes

In my own words, a nested route is a region within a page layout that responds to route changes. For example, in a single-page application, when navigating from one URL to another, you do not need to render the entire page, but only those regions within the page that are dependent on that URL change.

Wireframe showing page layout consisting of three sections: the main layout (1), the navigation menu (2) the page contents (3) and the header (4).

In the wireframe above, when clicking on a header link (4), the main content (1) will be rendered to show the content for this route, while the header remains unchanged. Similarly, when clicking on the left navigation links (2), the page’s content section (3) will update to show the new content, but the header, footer, and navigation menu remain unchanged.

This layout could be implemented in several ways.

export default function App() {
  return (
    <div className="app">
      <BrowserRouter>
        <Routes>
          <Route path="/catalog/:id" element={<Catalog />} />
          <Route path="/catalog" element={<Catalog />} />
          <Route path="/welcome" element={<Welcome />} />
          <Route index element={<Home />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

Flat routing structure - v6 | Flat routing structure - v5

In the above flat structure, we have four routes declared in a single file. /catalog/:id and /catalog would render the <Catalog> component which includes the left navigation and the content area. If the :id param is present, then it would show the content for that :id, if not it would show some default content. /welcome shows a welcome message and the final catch-all route shows the home page, including the <Header> and <Footer>.

Navigating between the different routes would cause the main section (1) to render with the updated content. This includes the <Header>, <Footer>, and <Nav> - even if they are not changing. If you play around with the demo, you will probably feel that it works well, is snappy and there are no glaring bugs. This routing structure is fairly common and I have personally encountered it numerous times on production. However, this structure is not optimized and when navigating from one URL to another, the CPU is doing a lot of work that it doesn’t need to. In our example this overhead is negligible, but on a more complex application, it may result in visible jank & deteriorate the user experience.

To make re-renderings more apparent, I have added the following code snippet but initially left it commented out. If you are sensitive to flashing images, please use caution.

  React.useLayoutEffect(() => {
    if (ref && ref.current) {
      ref.current.style = "background-color: #fa9a9a;";

      setTimeout(() => {
        ref.current.style = "background-color: none;";
      });
    }
  });

Let’s get nested

The above routing structure could be optimized by using nested routes to avoid rendering components that have not changed. As a default rule, we only want to render what has changed. When a user clicks on the left navigation links, the only component we want to render is the content section. Similarly, when a user clicks on a header link, we only render the main section.

export default function App() {
  return (
    <div className="app">
      <BrowserRouter>
        <Routes>
          <Route path="/welcome" element={<Welcome />} />
          <Route path="*" element={
            <Header />
            <Routes>
              <Route path="/catalog/*" element={
                <div className="two-column" ref={ref}>
                  <Nav />
                  <div className="content">
                    <Routes>
                      <Route path=":id" element={<Content />} />
                      <Route
                        index
                        element={<p>Use the left nav to selet a catalog item</p>}
                      />
                    </Routes>
                  </div>
                </div>
              } />
              <Route index element={<Home />} />
            </Routes>
            <Footer />
          } />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

Nested routes (v6) | Nested routes (v5)

Instead of having three routes on a single level, we now have six routes spread over three levels. At the topmost level, we have two routes, path="*" and path="/welcome". These two routes were separated because the <Header> and <Footer> are not visible on the <Welcome> page.

On the second level, we have two routes, path="/catalog/*" and index. These are used to render the <Catalog> or <Home> respectively. As you can see in the code snippet above, the <Header> and <Footer> are included in the element attribute for path="*" instead of being declared within <Catalog> and <Home> as we had done in the flat-structure.

Finally, on the inner-most level, there are two more routes. The first path exposes the :id param with path=":id". Since this route is a nested route of path="/catalog/*", then the path is built onto its parent’s, matching in /catalog/:id. The index route is used when no :id is present.

If you experiment with the demo, you will see that each component is only rendered when needed, making this solution much more optimized than the one we saw earlier. I love it!

Conclusion

Nested routes aren’t a new concept. If I remember correctly, I was using some form of nested routes way back in 2009 on C#'s MVC framework (it’s been a while so let me know if I’m mixing things up). Yet I still encounter cases when developers opt for a flat structure when nested routes would be the better solution. While I believe that nested routes could help you today, I expect that nested routes would become even more important in the near future, with concepts such as Islands Architecture and frameworks like Remix gaining traction. Give it a go and you won’t turn back.

Thank you for reading & have a good one!

© 2022 Kevin Farrugia