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

To understand recursion, one must first understand recursion experience months of pain and confusion. The same may be true for understanding recursive routes – though hopefully this post can take the edge off.

It may seem impractical, but having the ability to render recursive routes will serve as both a solid exercise to solidify your understanding of React Router as well as give you the ability to solve potentially tricky UI problems down the road. When would you ever want to render recursive routes? Well, like porn, you’ll know it when you see it.

Pre-reqs

This is an advanced post. Before you read this, make sure you’re familiar with URL Parameters and Nested Routes with React Router before continuing.

The main idea here is that since React Router is just components, theoretically, you can create recursive, and therefore infinite, routes. The secret here lies in setting up the correct data structure. In this example, we’ll use an array of users who all have an id, a name, and an array of friends.

const users = [
{ id: 0, name: 'Michelle', friends: [1, 2, 3] },
{ id: 1, name: 'Sean', friends: [0, 3] },
{ id: 2, name: 'Kim', friends: [0, 1, 3], },
{ id: 3, name: 'David', friends: [1, 2] }
]

By having our data structure set up this way, when we render a Person, we’ll render all of their friends as Links. Then, when a Link is clicked, we’ll render all of that person’s friends as Links - then it’s turtles all the way down. 🐢

Each time a Link is clicked, the app’s pathname will become progressively longer.

Here’s how it’ll look. Initially, we’ll be at / and the UI will look like this

Michelle'sFriends
* Sean
* Kim
* David

If Kim is clicked, then the URL will change to /2 (Kim’s id) and the UI will look like this

Michelle'sFriends
* Sean
* Kim
* David
Kim'sFriends
* Michelle
* Sean
* David

If David is clicked, then the URL will change to /2/3 (Kim’s id then David’s id) and the UI will look like this

Michelle'sFriends
* Sean
* Kim
* David
Kim'sFriends
* Michelle
* Sean
* David
David'sFriends
* Sean
* Kim

And this process repeats for as long as the user wants to click on Links.

Now that we have the right data structure and mental model in place, the next thing to do it construct our initial Routes. As we just saw, we want the main kickoff point of our app to be /:id. The component that’s going to be rendered at that path (and eventually do all the heavy lifting of creating our nested Routes and Links) is our Person component.

import {
BrowserRouter as Router,
Routes,
Route,
Link
} from 'react-router-dom'
const users = [
{ id: 0, name: 'Michelle', friends: [1, 2, 3] },
{ id: 1, name: 'Sean', friends: [0, 3] },
{ id: 2, name: 'Kim', friends: [0, 1, 3], },
{ id: 3, name: 'David', friends: [1, 2] }
]
const Person = () => {
return (
<div>
PERSON
</div>
)
}
export default function App() {
return (
<Router>
<Routes>
<Route path="/:id" element={<Person />}>
</Routes>
</Router>
)
}

Now, before we continue with our Person component, let’s make one small addition. As we just saw, the main kickoff point of our app is /:id. This is what we want, but it’s a little strange to have nothing at the main index route, /. Let’s set up a simple redirect so if the user visits /, they’ll be taken to /0.

import {
...
Navigate
...
} from 'react-router-dom'
export default function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Navigate to="/0" />} />
<Route path="/:id" element={<Person />} />
</Routes>
</Router>
)
}

Now comes the fun part, implementing our Person component.

Remember, there are a few things this component needs to be responsible for.

1) Using the id URL parameter, it needs to find that specific person in the users array.

2) It should render a Link for every one of that specific person’s friends.

3) It should render a Route which will match for the current pathname + /:id.

Let’s tackle #1. We know the id of the person we need to grab because of the URL parameter. Next, using that id, we can use Array.find to grab the person out of the users array.

import {
...
useParams
...
} from 'react-router-dom'
const Person = () => {
const { id } = useParams()
const person = users.find((p) => p.id === Number(id))
return (
<div>
PERSON
</div>
)
}

Next up we need to map over the person’s friends and create a Link for each one of them. Because React Router supports relative Links, we don’t need to do anything fancy here, just leave off the / so React Router knows to append id to the current URL.

const Person = () => {
const { id } = useParams()
const person = users.find((p) => p.id === Number(id))
return (
<div>
<h3>{person.name}’s Friends</h3>
<ul>
{person.friends.map((id) => (
<likey={id}>
<Link to={id}>
{users.find((p) => p.id === id).name}
</Link>
</li>
))}
</ul>
</div>
)
}

Finally, as stated in #3, we need to render a nested Route to match the pattern of our newly created Links. Similar to what we did with our nested Link, we’ll leave off the beginning / so React Router knows we want our Route to be relative.

const Person = () => {
const { id } = useParams()
const person = users.find((p) => p.id === Number(id))
return (
<div>
<h3>{person.name}’sFriends</h3>
<ul>
{person.friends.map((id) => (
<li key={id}>
<Link to={id}>
{users.find((p) => p.id === id).name}
</Link>
</li>
))}
</ul>
<Routes>
<Route path={`:id`} element={<Person />} />
</Routes>
</div>
)
}

At this point, we’re very close to being done. However, if you try to run our app as is, you’ll notice it doesn’t work.

There are two important changes we need to make to our code, and they both have to do with how React Router handles nested routes. Whenever you render a Route that is going to have a nested Routes somewhere in its descendant tree, you need to append /* to the URL to tell React Router to build upon the current path.

We’ll need to make this change in both areas where we render a Route.

const Person = () => {
const { id } = useParams()
const person = users.find((p) => p.id === Number(id))
return (
<div>
<h3>{person.name}’sFriends</h3>
<ul>
{person.friends.map((id) => (
<li key={id}>
<Link to={id}>
{users.find((p) => p.id === id).name}
</Link>
</li>
))}
</ul>
<Routes>
<Route path={`:id/*`} element={<Person />} />
</Routes>
</div>
)
}
export default function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Navigate to="/0" />} />
<Route path="/:id/*" element={<Person />} />
</Routes>
</Router>
)
}

That’s it. Person renders a list of Links as well as a Route matching any of those Links. When a Link is clicked, the Route matches which renders another Person component which renders a list of Links and another Route. This process continues as long as the user continues to click on any Links.

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.