Topic 13.4
Nested Routes and Layouts
In one line
Nested routes put routes inside routes, so a parent renders the shared frame (header, sidebar, tabs) and an <Outlet /> where the matching child appears; index routes fill the outlet at the parent's own URL, layout routes add a frame without a URL segment, * catches unknown paths, and links inside a route are relative to it.
Think of it like this
A picture frame with a slot for photos. The frame (the layout) stays on the wall, and you slide a different photo (the child page) into the slot. A big frame can hold a smaller frame with its own slot, like a dashboard inside the site. When the URL changes, only the photo in the slot changes; the frames stay where they are.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Nested route
- A route written inside another route. Its path is added to the parent's path.
- Outlet
<Outlet />: the spot in a parent route's element where the matching child route renders.- Index route
- A child route with
indexinstead ofpath. It renders at the parent's own URL, as the default child. - Layout route
- A route with an element but no path: it wraps its children in a shared frame without changing the URL.
- Not-found route
- A route with
path="*"that catches any URL no other route matches. - Relative link
- A
towithout a leading slash, resolved from the route the link is rendered in. - `..`
- In a link, "go up one route level". With
relative="path", "go up one URL segment". - Outlet context
- A value a parent passes with
<Outlet context={...} />, read by the child withuseOutletContext().
Step by step
01Draw the screen as nested boxes
Before writing routes, sketch the page: which parts stay the same on which pages. Each box that holds changing content becomes a route with an <Outlet />.
02Write the routes the same shape
The JSX tree of <Route>s is the same tree as the drawing. Children have relative paths.
<Routes>
<Route element={<SiteLayout />}> {/* layout route: no path */}
<Route index element={<Home />} /> {/* "/" */}
<Route path="dashboard" element={<Dashboard />}>
<Route index element={<Overview />} /> {/* "/dashboard" */}
<Route path="settings" element={<Settings />} /> {/* "/dashboard/settings" */}
</Route>
<Route path="*" element={<NotFound />} />
</Route>
<Route path="login" element={<Login />} /> {/* no site header here */}
</Routes>03Put an Outlet in every parent
The parent's element decides where the child goes. Anything around the outlet is shared by all children.
import { NavLink, Outlet } from "react-router";
export function Dashboard() {
return (
<div className="dashboard">
<aside>
<NavLink to="/dashboard" end>Overview</NavLink>
<NavLink to="/dashboard/settings">Settings</NavLink>
</aside>
<main><Outlet /></main>
</div>
);
}04How a URL becomes a stack of components
For /dashboard/settings the router finds the matching branch (SiteLayout → Dashboard → Settings) and renders each element into its parent's outlet. Moving to /dashboard swaps only the innermost element.
05Link relatively
Inside nested features, prefer relative to values. They follow the route tree, so moving a feature to a new URL prefix doesn't break its links.
// rendered by <Route path=":projectId/tasks"> inside <Route path="projects">
<Link to="edit">Edit</Link> // /projects/7/tasks/edit
<Link to="..">All projects</Link> // /projects (parent route)
<Link to=".." relative="path">Project 7</Link> // /projects/7 (one segment up)06Share data with the child
If the parent already has data the children need (the current project, the logged-in user), pass it through the outlet instead of fetching it again.
function ProjectLayout() {
const project = useProject();
return <Outlet context={{ project }} />;
}
function ProjectTasks() {
const { project } = useOutletContext();
return <h2>{project.name}: tasks</h2>;
}Try it yourself
- 1
Remove the Outlet
In the first example, delete
<Outlet />fromDashboard. Predict what/dashboard/settingsshows. Then put it back above the<nav>and see the child move. - 2
A dashboard-only 404
Add
<Route path="*" element={<p>No such dashboard page</p>} />inside the dashboard route, and a link to/dashboard/nope. Predict which 404 wins for/dashboard/nopeand for/nope. - 3
Move the feature
In the relative links example, change
path="projects"topath="admin/projects"andinitialEntriesto["/admin/projects/7/tasks"]. Predict the three hrefs. You didn't touch a single link.
Code & diagrams
The header (layout route) stays on every page. On `/dashboard` the index route fills the dashboard's outlet. The unknown `/oops` falls through to `*`, still inside the site layout.
Preview
Console
Nothing logged yet.Expected console output
/ | Home page
/dashboard | Dashboard > Overview: 3 new orders
/dashboard/settings | Dashboard > Settings: dark mode on
/oops | 404: page not foundLook at the generated `href`s: `..` climbs to the parent **route** (`projects`), skipping two segments because `:projectId/tasks` is one route. `relative="path"` climbs one URL segment. The links stay the same after clicking Edit because they are relative to the route they're rendered in, not to the URL.
Preview
Console
Nothing logged yet.Expected console output
Edit -> /projects/7/tasks/edit
Up one route -> /projects
Up one path segment -> /projects/7
after Edit: /projects/7/tasks/edit | Editing tasksconst routes = [
{
Component: SiteLayout, // layout route: no path
children: [
{ index: true, Component: Home },
{
path: "dashboard",
Component: Dashboard,
children: [
{ index: true, Component: Overview },
{ path: "settings", Component: Settings },
],
},
{ path: "*", Component: NotFound },
],
},
];No routes matched location "/oops" Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A parent without an Outlet
Write function Dashboard() { return <h1>Dashboard</h1>; } (no <Outlet />) and visit /dashboard/settings.
Break #2
Leading slash on a child path
Inside <Route path="dashboard">, write the child as <Route path="/settings" element={<Settings />} />.
Myth vs fact
Myth
Nested routes need nested URLs only.
Fact
Layout routes (no path) nest components without adding URL segments, and a single child path can have several segments (:projectId/tasks).
Myth
to=".." always removes one segment from the URL.
Fact
By default .. goes up one route level, which may be several segments. Use relative="path" for one segment.
Myth
The index route is the same as a route with path="".
Fact
An index route renders only at the parent's exact URL, can't have children, and gets an ?index marker in form actions (Topic 13.5). Use index, not an empty path.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Parent elements stay mounted while children change, which preserves their state but also means effects in a layout don't re-run on child navigation. Use
useLocation()in the layout if it must react to every page change. - ▸
useOutlet()returns the child element (ornull) instead of rendering it, handy for animating page transitions or showing a placeholder when no child matched.useMatches()(data routers) lists every matched route with itshandle, which is how breadcrumbs are usually built. - ▸
Descendant
<Routes>: a component rendered bypath="dashboard/*"can contain its own<Routes>whose paths are relative to/dashboard. This lets big features own their routes, but the router can't see those routes ahead of time, so data routers can't load their data; prefer one route tree in data mode. - ▸
Relative resolution happens against the route hierarchy, which is why
..from a route with a multi-segment path jumps more than one segment. This was thev7_relativeSplatPathchange: since v7, relative links inside splat (*) routes resolve from the full matched path.
Remember this
- 1
Nested routes are
<Route>elements written inside another<Route>. The child's path is added to the parent's: asettingsroute insidedashboardmatches/dashboard/settings. Child paths are written without a leading slash, because they are relative to the parent. When the URL matches a child, React Router renders both: the parent's element, and the child's element in the place where the parent put an **<Outlet />**.<Outlet />is the slot: it renders whichever child route matched, or nothing if none did. A parent without an<Outlet />simply never shows its children, a common bug. - 2
Why nest? Pages in real apps share frames: every page has the site header, every dashboard page has the dashboard sidebar, every settings page has the settings tabs. With nesting, each frame is written once and stays mounted while you move between its children. Its state (a collapsed sidebar, a scroll position, a search box) is kept, and only the inner part re-renders. The URL structure (
/dashboard/settings/profile) mirrors the component structure (Site → Dashboard → Settings → Profile). - 3
An index route is a child with the
indexprop instead of apath:<Route index element={<Overview />} />. It renders in the parent's outlet when the URL is exactly the parent's path. On/dashboardthe outlet would otherwise be empty; the index route is the default child, like the first tab being selected. Index routes can't have children of their own. ANavLinkto the parent path should useend, or it would stay active on every child page too. - 4
A layout route is a parent route with an
elementbut **nopath**. It doesn't add anything to the URL; it only wraps its children in a shared frame. Typical uses: aSiteLayoutwith header and footer around most pages but not the login page, or anAuthLayoutthat checks the user is logged in before rendering its children (Topic 13.6). Layout routes can be nested at any level, and you can have several side by side to give different groups of pages different frames. - 5
A not-found route uses
path="*": the splat matches any path, but ranks lowest, so it only wins when nothing else matches. Put one at the top level for unknown URLs ("Page not found" with a link home), and you can put more inside nested routes (dashboard/*unknowns can show inside the dashboard frame). Without one, an unknown URL renders an empty outlet: a blank page with no explanation. In a real development build React Router also logs a warning in the console:No routes matched location "/oops". - 6
Relative links. A
<Link to="edit">(no leading/) is resolved from the route it is rendered in, not from the current URL. Inside the routeprojects/:projectId/tasks,to="edit"becomes/projects/7/tasks/edit.to=".."goes up one route level (to the parent route's path, here/projects), which may skip several URL segments; addrelative="path"to go up one path segment instead (/projects/7). Relative links make nested features portable: move the wholeprojectstree under/adminand its links still work. Parents can also pass values down to whatever child is in the outlet with **<Outlet context={value} />anduseOutletContext()**, a small built-in context (Topic 9.2) for that one parent-child pair.
Explain it without notes
How do nested routes and <Outlet /> work together?
What is the difference between an index route and a layout route?
How do you handle unknown URLs in React Router?
How are relative links resolved, and what does relative="path" change?
Practice
Build a /settings section with tabs Profile (index) and Security (security), shown inside a settings frame with a heading, plus a top-level not-found page.
Make an AdminLayout layout route that adds a red "Admin" banner to /users and /reports but not to /.
Inside the route orders/:orderId, write links to the order's invoice child, to the orders list, and to /.
Trade-offs
- ↔
Deep nesting keeps shared UI in one place, but very deep trees make it hard to see which component renders where. Nest where the screen really nests; keep unrelated pages flat.
- ↔
Outlet context is quick for passing a parent's data to its children, but it couples them and isn't typed by default. For data many routes need, use loaders with
useRouteLoaderData(data mode) or real context.
Done when you can
I can nest routes and render children with
<Outlet />.I can use index routes as default children.
I can add a layout route that wraps pages without changing URLs.
I always add a
*not-found route.I can write relative links with
..andrelative="path".I can pass data to the child with outlet context.