Command Palette

Search for a command to run...

Hectal
PHASE 13Intermediate ~29 min· topic 4 of 6

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 index instead of path. 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 to without 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 with useOutletContext().

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 />.

Draw the screen as nested boxesdiagram
Rendering diagram…

02Write the routes the same shape

The JSX tree of <Route>s is the same tree as the drawing. Children have relative paths.

App.jsxwhole filejsx
<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.

Dashboard.jsxwhole filejsx
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.

How a URL becomes a stack of componentsdiagram
Rendering diagram…

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.

Project.jsxwhole filejsx
// 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.

Projects.jsxwhole filejsx
function ProjectLayout() {
  const project = useProject();
  return <Outlet context={{ project }} />;
}

function ProjectTasks() {
  const { project } = useOutletContext();
  return <h2>{project.name}: tasks</h2>;
}

Try it yourself

  1. 1

    Remove the Outlet

    In the first example, delete <Outlet /> from Dashboard. Predict what /dashboard/settings shows. Then put it back above the <nav> and see the child move.

  2. 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/nope and for /nope.

  3. 3

    Move the feature

    In the relative links example, change path="projects" to path="admin/projects" and initialEntries to ["/admin/projects/7/tasks"]. Predict the three hrefs. You didn't touch a single link.

Code & diagrams

Layout route, nested dashboard, index routes and a 404 New tab

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

Press Run to see the page.

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 found
Relative links and outlet context New tab

Look 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

Press Run to see the page.

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 tasks
The same tree as route objects (data mode, Topic 13.5)tsx
const 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 },
    ],
  },
];
Console in a real dev build when nothing matches (no * route)text
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.

terminal
$ Page
── what you'll see ──
Dashboard
(the Settings content never appears, and no error is printed)

Break #2

Leading slash on a child path

Inside <Route path="dashboard">, write the child as <Route path="/settings" element={<Settings />} />.

terminal
$ Console
── what you'll see ──
Uncaught Error: Absolute route path "/settings" nested under path "/dashboard" is not valid. An absolute child route path must start with the combined path of all its parent routes.

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 (or null) 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 its handle, which is how breadcrumbs are usually built.

  • ▸

    Descendant <Routes>: a component rendered by path="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 the v7_relativeSplatPath change: since v7, relative links inside splat (*) routes resolve from the full matched path.

Remember this

  1. 1

    Nested routes are <Route> elements written inside another <Route>. The child's path is added to the parent's: a settings route inside dashboard matches /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. 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. 3

    An index route is a child with the index prop instead of a path: <Route index element={<Overview />} />. It renders in the parent's outlet when the URL is exactly the parent's path. On /dashboard the 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. A NavLink to the parent path should use end, or it would stay active on every child page too.

  4. 4

    A layout route is a parent route with an element but **no path**. It doesn't add anything to the URL; it only wraps its children in a shared frame. Typical uses: a SiteLayout with header and footer around most pages but not the login page, or an AuthLayout that 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. 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. 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 route projects/: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; add relative="path" to go up one path segment instead (/projects/7). Relative links make nested features portable: move the whole projects tree under /admin and its links still work. Parents can also pass values down to whatever child is in the outlet with **<Outlet context={value} /> and useOutletContext()**, a small built-in context (Topic 9.2) for that one parent-child pair.

Explain it without notes

01

How do nested routes and <Outlet /> work together?

02

What is the difference between an index route and a layout route?

03

How do you handle unknown URLs in React Router?

04

How are relative links resolved, and what does relative="path" change?

Practice

01

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.

02

Make an AdminLayout layout route that adds a red "Admin" banner to /users and /reports but not to /.

03

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 .. and relative="path".

  • I can pass data to the child with outlet context.