How to create deep paths in a tree structure in react-router v6
I found an interesting problem when constructing routes and links for a recursive data structure that would work for any depth in react-router v6.
Here is a codesandbox with the code used in this post.
The routes in question were for a tree-view component that initially starts life like the screenshot below:
Each tree node allows the user to click on a node and view only that node and its children in isolation. The requirements dictated that each node must have a unique URL to enable the user to link to that particular node.
For example, if the user selects node three
, then the URL should change to /tasks/3
, and that section of the tree is displayed:
.
If the user then selects five
, the URL needs to change to tasks/3/children/5
, and that section of the tree is displayed:
The tree depth is unknown, so we need to accommodate any depth that might have a URL like /tasks/4/children/6/children/8/children/10
....etc.
Clicking the back
link moves the user up a section of the tree, so if they are currently on tasks/3/children/5/children/7
, then clicking back should move them to tasks/3/children/5
.
React Router V6 useRoutes
We declare the initial App
component using the new useRoutes hook that allows the developer to create a route config object instead of nested <Route />
components.
const routes: RouteObject[] = [
{
path: "/tasks",
element: <Layout />,
children: [
{
path: ":id/*",
element: <TaskItem />
},
{
index: true,
element: <TaskTree tasks={tasks} />
}
]
},
{
index: true,
element: <Navigate to="/tasks" />
}
];
export function App() {
const element = useRoutes(routes);
return element;
}
Line 7
highlights an interesting path pattern with a *
on the end and matches any route like /tasks/3
.
The other element (lines 16-19) in the children
array is an index route. Index routes have no path
property and render in their parent's route.
I will omit the TreeView
code to keep the lines of code down in the following code snippets.
If I were just rendering a list of links, it would look like this:
function TaskTreePage() {
return (
<ul>
{tasks.map(({ id, name }) => (
<li key={name}>
<Link to={`${id}`}>
<h2>{name}</h2>
</Link>
</li>
))}
</ul>
);
}
Recursive routes
Upon clicking any of the links, the :id/*
route will match this part of our route config:
children: [
{
path: ":id/*",
element: <TaskItem />
},
The TaskItem
component will render:
function TaskItem() {
const { id } = useParams();
const task = findTask(id as string, tasks);
return (
<Routes>
<Route
index
element={
<div>
<h2>task of {task.name}</h2>
<div>
<Link to="../..">Back</Link>
<div>
<h2>Children</h2>
{task.children.map((child: Task) => (
<Link key={child.id} to={`children/${child.id}`}>
{child.name}
</Link>
))}
</div>
</div>
</div>
}
/>
<Route path="children/:id/*" element={<TaskItem />} />
</Routes>
);
}
The TaskItem
component renders a new Routes
component with two routes. The first route is an index route to render the tree (or links in this example) for the current depth. The other route on line 26 is to render the nested children.
The route on line 26 is where the magic happens. The TaskItem
component is recursively adding a route that will link to another instance of itself in a nested route.
<Route path="children/:id/*" element={<TaskPage />} />
The trailing wildcard matcher *
in path="children/:id/*"
matches any nested route, and the path appends onto the previous TaskItem
instance's route path.
Another interesting point is the back Link
component:
<Link to="../..">Back</Link>
The to
prop is ../..
and not ..
since each new nesting level adds two new path segments, e.g. from /tasks/3
, we could go to /tasks/3/children/5
, and then the backlink would return us to/tasks/3`.
Summary
Here is a link to the codesandbox that illustrates several new features which will supercharge your react-router v6 learning.