Because React Router is just components, you can do crazy things like having recursive routes. In this post we'll learn how they work by breaking down the 'Recursive Paths' example on the React Router docs.
Recursive routes aren't the most pragmatic thing in the world, but they really show off the benefits of React Router's component based approach to routing.
The main idea here is that since React Router is just components, theoretically, you can create recursive and therefor infinite routes. The secret lies in setting up the right data structure which can lead to infinite routes. In this example we'll use an array of people 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
* David
Kim'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
* David
Kim's Friends
* Michelle
* Sean
* David
David's Friends
* Sean
* Kim
And this process repeats for as long as the user wants to click on Link
s.
Once you have the right data structure set up, the next important step is to continually render a Route
and some Links
s. Because we're creating infinite routes, we'll need to make sure we have a Route
that is rendered every time a Link
is clicked. If not, we won't get any more matches which means React Router won't render any more components. In both our Link
and our Route
we'll need to know the app's current pathname so that we can append to it every time a Link
is clicked (like in the example above, we went from /2
to /2/3
, and on). Luckily for us, React Router gives us the pathname with match.url
. With that in mind, the initial part of our Link
will look like this
<Link to={`{match.url}/${id}}>
and the Route
we render will match on a similar pattern then render the same component.
<Route path={`${match.url}/:id`} component={Person}/>
Now that we have the basics down, let's start building out the component which is going to be recursively rendered, Person
.
Remember, there's a few things this component needs to be responsible for.
1) It should render a Link component for every one of that specific person's friends.
2) It should render a Route component which will match for the current pathname + /:id.
As with all recursive problems, we need to somehow "kick off" the recursion. Typically this involves invoking the function but if it's a component that's being called recursively, we can do that by simply creating the element.
import 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 = ({ match }) => {
return (
<div>
PERSON
</div>
)
}
class App extends React.Component {
render() {
return (
<Router>
<Person />
</Router>
)
}
}
export default App
Now what we need to do is figure out how to get the specific friend's information from our users
array so we can grab their name and render their friends. You may notice a problem here. Eventually Person
is going to be rendered by React Router so it'll be passed a match
prop. It's this match
prop we'll use to get the current pathname and (with help from users
) the person's name and friends list. The problem is we're rendering Person
manually inside the main App
component to kick off the recursion. That means match
is going to be undefined the first time Person
is rendered. The solution to this problem is simpler than it may seem. When we first manually render <Person />
we'll need to pass it a match
prop just as React Router would.
class App extends React.Component {
render() {
return (
<Router>
<Person match={{ params: { id: 0 }, url: '' }}/>
</Router>
)
}
}
Now, everytime Person
is rendered, including the first time, it'll be passed a match
prop which will contain two things we need, url
for rendering our Route
and Link
s and params.id
so we can figure out which person is being rendered.
Alright back to the main goal at hand. Person
needs to
1) It should render a Link component for every one of that specific person's friends.
2) It should render a Route component which will match for the current pathname + /:id.
Let's tackle #1. Before we can render any Link
s, we need to get the person's friends. We already know the person's id
from match.params.id
. Using that knowledge with the Array.find
method means getting the friends info should be pretty straight forward. We'll create a helper function for it.
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 find = (id) => users.find(p => p.id == id)
const Person = ({ match }) => {
const person = find(match.params.id)
return (
<div>
PERSON
</div>
)
}
Slowly getting there. Now we have the person, let's render some UI including the Link
for each of their 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 ] }
]
const find = (id) => users.find(p => p.id == id)
const Person = ({ match }) => {
const person = find(match.params.id)
return (
<div>
<h3>{person.name}’s Friends</h3>
<ul>
{person.friends.map((id) => (
<li key={id}>
<Link to={`${match.url}/${id}`}>
{find(id).name}
</Link>
</li>
))}
</ul>
</div>
)
}
We're so close to being done. Now that we have a Link
for each of the person's friends, as mentioned in #2, we need to make sure we also render a Route
.
const Person = ({ match }) => {
const person = find(match.params.id)
return (
<div>
<h3>{person.name}’s Friends</h3>
<ul>
{person.friends.map((id) => (
<li key={id}>
<Link to={`${match.url}/${id}`}>
{find(id).name}
</Link>
</li>
))}
</ul>
<Route path={`${match.url}/:id`} component={Person}/>
</div>
)
}
The full code now looks like this
import React from 'react'
import {
BrowserRouter as Router,
Route,
Link
} from 'react-router-dom'
const find = (id) => users.find(p => p.id == id)
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 = ({ match }) => {
const person = find(match.params.id)
return (
<div>
<h3>{person.name}’s Friends</h3>
<ul>
{person.friends.map((id) => (
<li key={id}>
<Link to={`${match.url}/${id}`}>
{find(id).name}
</Link>
</li>
))}
</ul>
<Route path={`${match.url}/:id`} component={Person}/>
</div>
)
}
class App extends React.Component {
render() {
return (
<Router>
<Person match={{ params: { id: 0 }, url: '' }}/>
</Router>
)
}
}
export default App
The first time Person
is rendered, we pass it a mock match
object. Then, 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 theoretically forever as long as the user continues to click on any Link
s.
This was originally published at TylerMcGinnis.com and is part of their React Router course.
Top comments (2)
Very neat! Also, I hope to God I never see anybody do this for real.
It's probably more common than you realize - twitter.com/ryanflorence/status/10...