Check your version
This post assumes you're using React Router v5. If not, find your version below.Recursive routes aren't the most pragmatic thing in the world, but they really show off the benefits of React Router v5's component-based approach to routing.
If you're not familiar with nested routes, I'd check out Nested Routes with React Router v5 before continuing.
The main idea here is that since React Router v5 is just components, theoretically, you can create recursive, and therefore infinite routes. The secret lies in setting up the right data structure which can lead to the infinite routes. 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 this data structure set up this way, when we render a Person
, we'll render all of their friends as Link
s. Then, when a Link
is clicked, we'll render all of that person's friends as Link
s, and on and on. Each time a Link
is clicked, the app's pathname will become progressively longer.
Initially, we'll be at /
and the UI will look like this
Michelle's Friends* 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's Friends* Sean* Kim* DavidKim's Friends* 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's Friends* Sean* Kim* DavidKim's Friends* Michelle* Sean* DavidDavid's Friends* Sean* Kim
And this process repeats for as long as the user wants to click on Link
s.
Now that we have the right data structure and mental model for our app, the next thing to do it construct our initial Route
s. 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 Route
s and Link
s) is our Person
component. For now, we'll just keep it simple.
import * as React from "react";import { BrowserRouter as Router, 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><Route path="/:id"><Person /></Route></Router>);}
Now one small change before we start implementing our Person
component. 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
.
export default function App() {return (<Router><Route exact path="/"><Redirect to="/0" /></Route><Route path="/:id"><Person /></Route></Router>);}
Now comes the fun part, implementing our Person
component.
Remember, there are a few things this component needs to be responsible for.
- Using the
id
URL parameter, it needs to find that specific person in theusers
array. - It should render a
Link
for every one of that specific person's friends. - It should render a
Route
component 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.
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. The only "gotcha" here is what we pass as the to
prop to Link
. We want to make sure that we're taking the current URL, however deeply nested it is, and appending the id
of the person
we're mapping over to it. To grab the current URL, we can use React Router v5.1's useRouteMatch
custom Hook.
const Person = () => {const { id } = useParams();const person = users.find((p) => p.id === Number(id));const { url } = useRouteMatch();return (<div><h3>{person.name}’s Friends</h3><ul>{person.friends.map((id) => (<li key={id}><Link to={`${url}/${id}`}>{users.find((p) => p.id === id).name}</Link></li>))}</ul></div>);};
Finally, as stated in #3, we need to render a Route
to match the pattern of our newly created Link
s. Because we're creating a nested route, similar to what we did with Link
, we'll want to make sure we append the URL parameter (/:id
) to the app's path
up until that point. To get the path
, we can use the useRouteMatch
custom Hook.
const Person = () => {const { id } = useParams();const person = users.find((p) => p.id === Number(id));const { url, path } = useRouteMatch();return (<div><h3>{person.name}’s Friends</h3><ul>{person.friends.map((id) => (<li key={id}><Link to={`${url}/${id}`}>{users.find((p) => p.id === id).name}</Link></li>))}</ul><Route path={`${path}/:id`}><Person /></Route></div>);};
That's it. Person
renders a list of Link
s as well as a Route
matching any of those Link
s. When a Link
is clicked, the Route
matches which renders another Person
component which renders a list of Link
s and a new Route
. This process continues as long as the user continues to click on any Link
s.