How to Create Nested Routes with React Router and Hooks — An In Depth Project Summary
I walk through how I created a note-taking app with nested routes using react-router-dom and react-router hooks.
I wrote several blog posts recently about implementing react-router to create nested routes in a note-taking app. Since at the time I was still working through understanding how react-router works, I wanted to write a new summary post that lays out more clearly how I’ve implemented react-router in this app.
There are some decent observations in those previous posts, but I think this post should be most the complete and coherent, since the app is finally done and deployed. You can check out the live demo here: Chromatic Notes and the code here on Github.
This is the structure of the app, the component hierarchy. For the sake of clearly remembering for myself how this is all strung together in the future, and hopefully to help any readers understand more clearly, I have overlaid onto the component hierarchy the routing relationships between components. There are those components that contain react-router Links, and other components that contain react-router Routes, which wrap child components.
Route children are rendered when a Route’s path prop corresponds to the URL in the address bar. The address bar URL is changed by clicking a Link, which I believe just returns a simple HTML <a> tag.
Remember, the Link fires first, the Route reacts and renders its child.
That diagram is fairly abstract. I think it’s easier to understand routing by looking at the app’s actual UI, and then alternating between looking at that and looking back at the diagram.
Above, you can see the main Link-bearing components, which are really collections of components made by transforming arrays into React components. The collection to the left is a grid and on the right we have a list. On the left side if you click on the dark green NotebookCard, the right side displays the dark green TableOfContents.
How does this happen?
In App.js, we map over an array of notebooks to create the left side grid, transforming each notebook POJO (plain old JS object) into a NotebookCard component. The NotebookCard itself has a react-router Link component, whose “to” prop points to a URL that corresponds to the TableOfContents component of a particular color (or to be precise, the URL corresponds to the path prop of the Route enclosing the particular TableOfContents component). That Link looks like this: <Link to={`/${props.color}`} >
In App.js, we also map over that same array of notebooks to create a Route component for each notebook; each of these Route components wraps a TableOfContents component, and each Route has a path prop pointing to a path value using the notebook’s color property as a unique identifier, like so: path={`/${notebook.color}`}
This pattern repeats but it gets a little bit harder at the next level where we are nested deeper in the app. In the TableOfContents component, instead of mapping over an array of notebooks, we map over each notebook’s notes array to generate two types of child components: 1) a list of NotePreviewCards enclosing Links that will populate the TableOfContents, and also a list of NotePage components, each one enclosed in a Route, which will render the NotePage and replace the TableOfContents component on the screen when the Link ( 📖 ) in the NotePreviewCard is clicked.
So remember, the Link fires first, the Route reacts and renders its child.
What is slightly harder here, is that since we are nested, we now have to continue appending unique identifiers onto other unique identifiers in our Links and Routes. We either need to pass down routerProps to the components that need it, or we can instead use react-router hooks.
What is nice about using react-router hooks is that it frees us from having to prop drill routerProps, but rather in the component that needs access to routerProps, we can simply invoke hooks like useRouteMatch and useParams.
This is exactly what I did inside of the NotePreviewCard, which needs to access the URL property in order to pass it as a prop to its Link. Since NotePreviewCard is not a route itself, I don’t know of a good way to get it routerProps except by passing routerProps down from a parent component rendered by a Route, i.e., prop drilling.
To elaborate, for the NotePage component, since it is getting rendered by a route component, we naturally have access to routerProps by rendering the NotePage component inside of the route’s render prop.
You can see the difference here:
After merging routerProps into NotePage’s props, if I console.log props in the NotePage component, you can see that within its props, I have access to all of the keys of the routerProps object, i.e., the history, location, and match objects.
Since I do not have access to routerProps in the NotePreviewCard, since it’s not wrapped by a Route, I would have to use prop drilling to merge routerProps into its own props. Instead of this, I can just invoke a react-router hook inside of the NotePreviewCard, and in that way cleanly get access to exactly the piece of information from routerProps that I need, instead of getting the entire object.
URLs go with the Link components, so in NotePreviewCard, I invoke useRouteMatch and pull the URL key out of the match object.
With that, I build the correct link by appending props.id (the unique id for the note) onto the url (the unique identifier for the TableOfContents. e.g., “dark-green”) The resulting concatenated url would look like “/dark-green/33” or “/light-blue/2”.
When that Link is clicked, the address bar url changes, and the corresponding Route renders its child component, a NotePage, on the right side:
Helpful resources: Nested Routes with React Router v5