React Router and nested routes
Published: Tue Feb 01 2022Following 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.
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>
);
}
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>
);
}
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!
Latest Updates
- Contributing to the Web Almanac 2024 Performance chapterMon Nov 25 2024
- Improving Largest Contentful Paint on slower devicesSat Mar 09 2024
- Devfest 2023, MaltaWed Dec 06 2023
- Learn Performance course on web.devWed Nov 01 2023
- Setting up a Private WebPageTest instanceMon Jun 26 2023