Check your version
This post assumes you're using React Router v6. If not, find your version below.

A solid understanding of how, when, and why to create nested routes is foundational to any developer using React Router. However, in order to help us better answer those questions, there are some topics we need to cover first. Namely, you need to be comfortable with two of React Router’s most foundational components – Route and Routes.

Let’s start with Route. Put simply, Route allows you to map your app’s location to different React components. For example, say we wanted to render a Dashboard component whenever a user navigated to the /dashboard path. To do so, we’d render a Route that looked like this.

<Route path='/dashboard' element={<Dashboard />} />

The mental model I use for Route is that it always has to render something – either its element prop if the path matches the app’s current location or null, if it doesn’t.

Slow and steady wins the race
I realize we're starting off off slow here, but in doing so we'll set the proper foundation that we can build off of later. Pinky promise.

With Route out of the way, lets look at its friend – Routes.

<Routes>
<Route path="/" element={<Home />} />
<Route path="/messages" element={<Messages />} />
<Route path="/settings" element={<Settings />} />
</Routes>

You can think of Routes as the metaphorical conductor of your routes. Its job is to understand all of its children Route elements, and intelligently choose which ones are the best to render. It’s also in charge of constructing the appropriate urls for any nested Links and the appropriate paths for any nested Routes – but more on that in a bit.

Playing off our <Routes> above, say not only do we want a /messages page, but we also want a page for each individual conversation, /messages/:id. There are a few different approaches to accomplish this. Your first idea might be to just create another Route.

<Routes>
<Route path="/" element={<Home />} />
<Route path="/messages" element={<Messages />} />
<Route path="/messages/:id" element={<Chat />} />
<Route path="/settings" element={<Settings />} />
</Routes>

Assuming the UI for <Chat> had nothing to do with <Messages>, this would work. However, this is a post about nested routes, not just rendering normal routes.

Typically with nested routes, the parent Route acts as a wrapper over the child Route. This means that both the parent and the child Routes get rendered. In our example above, only the child Route is being rendered.

route style:

So to make a truly nested route, when we visit a URL that matches the /messages/:id pattern, we want to render Messages which will then be in charge of rendering Chat.

Real world example

A real-life example of this UI could look similar to Twitter’s /messages route. When you go to /messages, you see all of your previous conversations on the left side of the screen. Then, when you go to /messages/:id, you still see all your messages, but you also see your chat history for :id.

So how could we adjust our code to do this? Well, what’s stopping us from just rendering another Routes component inside our Messages component? Something like this:

// App.js
function App () {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/messages" element={<Messages />} />
<Route path="/settings" element={<Settings />} />
</Routes>
)
}
// Messages.js
function Messages () {
return (
<Container>
<Conversations />
<Routes>
<Route path='/messages/:id' element={<Chat />} />
</Routes>
</Container>
)
}

Now when the user navigates to /messages, React Router renders the Messages component. From there, Messages shows all our conversations via the Conversations component and then renders another Routes with a Route that maps /messages/:id to the Chat component.

Looks good, but there’s one subtle issue. Can you spot it?

Messages only gets rendered when the user is at /messages. When they visit a URL that matches the /messages/:id pattern, Messages no longer matches and therefore, our nested Routes never gets rendered.

To fix this, naturally, we need a way to tell React Router that we want to render Messages both when the user is at /messages or any other location that matches the /messages/* pattern.

Wait. What if we just update our path to be /messages/*?

// App.js
function App () {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/messages/*" element={<Messages />} />
<Route path="/settings" element={<Settings />} />
</Routes>
)
}

Much to our delight, that’ll work. By appending a /* to the end of our /messages path, we’re essentially telling React Router that Messages has a nested Routes component and our parent path should match for /messages as well as any other location that matches the /messages/* pattern. Exactly what we wanted.

There’s even one small improvement we can make to our nested Routes. Right now inside of our Messages component, we’re matching for the whole path – /messages/:id.

<Routes>
<Route path='/messages/:id' element={<Chat />} />
</Routes>

This seems a bit redundant. The only way Messages gets rendered is if the app’s location is already at /messages. It would be nice if we could just leave off the /messages part all together and have our path be relative to where it’s rendered. Something like this.

function Messages () {
return (
<Container>
<Conversations />
<Routes>
<Route path=':id' element={<Chat />} />
</Routes>
</Container>
)
}

As you probably guessed, you can do that as well since Routes supports relative paths. Notice we also didn’t do /:id. Leaving off the / is what tells React Router we want the path to be relative.


At this point, we’ve looked at how you can create nested routes by appending /* to our Route’s path and rendering, literally, a nested Routes component. This works when you want your child Route in control of rendering the nested Routes, but what if we didn’t want that?

Meaning, what if we wanted our App component to contain all the information it needed to create our nested routes rather than having to do it inside of Messages?

Because this is a common preference, React Router supports this way of creating nested routes as well. Here’s what it looks like.

function App () {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/messages" element={<Messages />}>
<Route path=':id' element={<Chats />} />
</Route>
<Route path="/settings" element={<Settings />} />
</Routes>
)
}

You declaratively nest the child Route as a children of the parent Route. Like before, the child Route is now relative to the parent, so you don’t need to include the parent (/messages) path.

Now, the last thing you need to do is tell React Router where in the parent Route (Messages) should it render the child Route (Chats).

To do this, you use React Router’s Outlet component.

import { Outlet } from 'react-router-dom'
function Messages () {
return (
<Container>
<Conversations />
<Outlet />
</Container>
)
}

If the app’s location matches the nested Route’s path, this Outlet component will render the Route’s element. So based on our Routes above, if we were at /messages, the Outlet component would render null, but if we were at /messages/1, it would render the <Chats /> component.

Cool... but which one is better?

Opinion Time: Though there’s no objective benefit to one approach over the other, I’d probably favor using the latter approach with <Outlet /> over the former nested Routes approach as it feels a little cleaner, IMO.


At this point, there’s nothing new about nested routes with React Router that you need to learn. However, it may be beneficial to see it used in a real app.

Here’s what we’ll be building. As you navigate around, keep an eye on the navbar. You’ll notice that we have the following URL structure.

/
/topics
:topicId
:resourceId

Now before we get started, let’s get a few housekeeping items out of the way first.

We’ll have an “API” which is responsible for getting us our data. It has three methods we can use, getTopics, getTopic, and getResource.

export function getTopics() {
return topics;
}
export function getTopic(topicId) {
return topics.find(({ id }) => id === topicId);
}
export function getResource({ resourceId, topicId }) {
return topics
.find(({ id }) => id === topicId)
.resources.find(({ id }) => id === resourceId);
}

If you’d like to see what topics looks like, you can do so here - spoiler alert, it’s just an array of objects which maps closely to our routes.

Next, our Home component for when the user is at the / route. Nothing fancy here either.

function Home() {
return (
<React.Fragment>
<h1>Home</h1>
<p>
Welcometoourcontentindex.
Headoverto{" "}
<Link to="/topics">
/topics
</Link> toseeourcatalog.
</p>
</React.Fragment>
);
}
Porque no los dos?

Because we’ve seen both patterns for creating nested routes, let’s see them both in our example as well. We’ll start out with the nested Routes pattern, then we’ll refactor to use the <Outlet /> pattern.

Next, we’ll build out our top level App component which will have our main navbar as well as Routes for / and /topics.

Looking at our final app, we know that / is going to map to our Home component and /topics is going to map to a component that shows our top level topics (which we can get from calling getTopics).

We’ll name this component Topics and since it’ll contain a nested Routes, we’ll make sure to append /* to the parent path.

function Topics () {
return null
}
export default function App() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
<hr />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/topics/*" element={<Topics />} />
</Routes>
</div>
</Router>
);
}

Now we need to build out the Topics component. As I just mentioned, Topics needs to show our top level topics which it can get from getTopics. Let’s do that before we worry about its nested routes.

import { Link } from 'react-router-dom'
import { getTopics } from './api'
function Topics() {
const topics = getTopics();
return (
<div>
<h1>Topics</h1>
<ul>
{topics.map(({ name, id }) => (
<li key={id}>
<Link to={id}>{name}</Link>
</li>
))}
</ul>
<hr />
</div>
);
}

Notice that because we’re using nested routes, our Link is relative to the location it’s rendered – meaning we can just do to={id} rather than having to do to={'/topics/${id}'}

Now that we know we’re linking to={id} (which is really /topics/react, /topics/typescript, or /topics/react-router), we need to render a nested Route that matches that same pattern.

We’ll call the component that gets rendered at the route Topic and we’ll build it out in the next step.

The only thing we need to remember about Topic is it’s going to also render a nested Routes, which means we need to append /* to the Route’s path we render in Topics.

function Topic () {
return null
}
function Topics() {
const topics = getTopics();
return (
<div>
<h1>Topics</h1>
<ul>
{topics.map(({ name, id }) => (
<li key={id}>
<Link to={id}>{name}</Link>
</li>
))}
</ul>
<hr />
<Routes>
<Route path=":topicId/*" element={<Topic />} />
</Routes>
</div>
);
}

We’re one level deeper and a pattern is starting to emerge.

Let’s build out our Topic component now. Topic will show the topic’s name, description, and then link its resources. We can get the topic by passing our topicId URL parameter we set up in the previous step to getTopic.

import { useParams } from 'react-router-dom'
import { getTopic } from './api'
function Topic() {
const { topicId } = useParams();
const topic = getTopic(topicId);
return (
<div>
<h2>{topic.name}</h2>
<p>{topic.description}</p>
<ul>
{topic.resources.map((sub) => (
<li key={sub.id}>
<Link to={sub.id}>{sub.name}</Link>
</li>
))}
</ul>
<hr />
</div>
);
}

Notice that even though we’re a few layers deep, our nested Links are still smart enough to know the current location so we can just link to={sub.id} rather than to={/topics/${topicId}/${sub.id}}

We’re almost there. Now we need to render our last nested Routes that matches the pattern we just saw. Again, because Routes is smart and supports relative paths, we don’t need to include the whole /topics/:topicId/ path.

function Resource () {
return null
}
function Topic() {
const { topicId } = useParams();
const topic = getTopic(topicId);
return (
<div>
<h2>{topic.name}</h2>
<p>{topic.description}</p>
<ul>
{topic.resources.map((sub) => (
<li key={sub.id}>
<Link to={sub.id}>{sub.name}</Link>
</li>
))}
</ul>
<hr />
<Routes>
<Route path=":resourceId" element={<Resource />} />
</Routes>
</div>
);
}

Finally, we need to build out the Resource component. We’re all done with nesting, so this component is as simple as grabbing our topicId and resourceId URL parameters, using those to grab the resource from getResource, and rendering some simple UI.

function Resource() {
const { topicId, resourceId } = useParams();
const {
name,
description,
id
} = getResource({ topicId, resourceId });
return (
<div>
<h3>{name}</h3>
<p>{description}</p>
<a href={`https://ui.dev/${id}`}>ReadPost</a>
</div>
);
}

Well, that was fun. You can find all the final code here.


Now, let’s throw all that out the window and refactor our app using the Outlet component. First, instead of having nested Routes sprinkled throughout our app, we’ll put all of them inside of our App component.

export default function App() {
return (
<Router>
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/topics">Topics</Link></li>
</ul>
<hr />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/topics" element={<Topics />}>
<Route path=":topicId" element={<Topic />}>
<Route
path=":resourceId"
element={<Resource />}
/>
</Route>
</Route>
</Routes>
</div>
</Router>
);
}

Now, we need to swap out the nested Routes inside of Topics and Topic for the <Outlet /> component.

function Topic() {
const { topicId } = useParams();
const topic = getTopic(topicId);
return (
<div>
<h2>{topic.name}</h2>
<p>{topic.description}</p>
<ul>
{topic.resources.map((sub) => (
<li key={sub.id}>
<Link to={sub.id}>{sub.name}</Link>
</li>
))}
</ul>
<hr />
<Outlet />
</div>
);
}
function Topics() {
const topics = getTopics();
return (
<div>
<h1>Topics</h1>
<ul>
{topics.map(({ name, id }) => (
<li key={id}>
<Link to={id}>{name}</Link>
</li>
))}
</ul>
<hr />
<Outlet />
</div>
);
}

And with that, we’re done. You can find the final code for using <Outlet> here.


To recap, nested routes allow you to, at the route level, have a parent component control the rendering of a child component. Twitter’s /messages route is the perfect example of this.

With React Router, you have two options for creating nested routes. The first is using the /* with nested <Routes> pattern and the second is using the <Outlet /> pattern.

Want to learn more?
If you liked this post and want to learn more, check out our free Comprehensive Guide to React Router.

Before you leave

I know, "another newsletter pitch" - but hear me out. Most JavaScript newsletters are terrible. When's the last time you actually looked forward to getting one? Even worse, when's the last time you actually read one rather than just skim it?

We wanted to change that, which is why we created Bytes. The goal was to create a JavaScript newsletter that was both educational and entertaining. 84,338 subscribers and an almost 50% weekly open rate later, it looks like we did it.

Delivered to 84,338 developers every Monday

Avatar for Tyler McGinnis

Tyler McGinnis

CEO of ui.dev. Obsessed with teaching, writing, swimming, biking, and running.

Share this post

Want more react-router?

This is part of our React Router course. You can take the rest of the course by starting a free 3-day trial.