skip to content

Nested and Dynamic Routes

Nested routes render children into an <Outlet>, and dynamic segments like :id turn URL structure into component hierarchy, with splat and optional segments for the edges. Interviewers use this to check you can map a URL scheme onto a layout without duplicating shells.

on this pageshow

explore

questions

5

In React Router v7, how do you configure /projects/:projectId/tasks/:taskId so every screen renders inside one shared dashboard shell?

level: juniorimportance: must knowfreq 68%

answer

  1. routes inside routes
  2. children paths without a leading slash
  3. the parent decides where the child goes
  4. index route for the bare parent URL

basics

~20 s

Nest the routes: a shell route whose element renders the sidebar and an <Outlet />, with projects/:projectId and then tasks/:taskId as descendants in children. Child paths are relative and each parent renders its matched child where it places <Outlet />.

solid answer

~40 s

Declare the URL as a tree. The top route's `element` is the dashboard shell, which renders the sidebar and an `<Outlet />`; its `children` include `projects/:projectId`, whose element renders the project header and its own `<Outlet />`; that route's `children` include `tasks/:taskId`. Child paths have **no leading slash**, because React Router joins them onto the parent's path, so the leaf matches `/projects/42/tasks/7`. An `index: true` child fills the outlet at the parent's exact URL, such as `/projects/42`. Each matched level renders into its parent's outlet, so the shell and sidebar are written once and stay mounted while the inner screens change. If a parent forgets `<Outlet />`, its children match but never appear.

code

tsx · 16 lines
tsx
import { NavLink, Outlet } from "react-router";

export function DashboardShell() {
  return (
    <div className="shell">
      <aside>
        <NavLink to="/projects/42">Billing</NavLink>
        <NavLink to="/projects/43">Onboarding</NavLink>
      </aside>
      <main>
        {/* the matched child route renders here */}
        <Outlet />
      </main>
    </div>
  );
}

go deeper

for a junior

Recall that nested routes are declared in children with relative paths, that the parent renders <Outlet /> where the child goes, and what an index route is for.

for a middle

Explain how paths are joined, the absolute-child-path invariant, multi-segment route paths and which elements render at each URL in the tree.

for a senior

Show judgment in structuring layers so shared chrome stays mounted, and diagnose an empty outlet or a duplicated header caused by flattened routes.

for a principal

Relate route-tree structure to team ownership: stable layout routes at the top let feature teams own subtrees without touching shared chrome.

## The shape of the configuration React Router v7 describes routes as a **tree**, whether you write route objects for `createBrowserRouter` or `<Route>` elements inside `<Routes>`. A route's `children` are matched against the part of the URL after the parent's path, and the element of each matched route is rendered **inside** its parent's element at the position of `<Outlet />`. For the dashboard, the URL `/projects/:projectId/tasks/:taskId` has three layers of UI: 1. the **dashboard shell**: top bar and sidebar, the same for every screen; 2. the **project frame**: project name, tabs, shown for anything under one project; 3. the **task screen** itself. Each layer becomes a route, and each non-leaf layer renders an `<Outlet />` where the next layer goes. ```tsx const router = createBrowserRouter([ { path: "/", element: <DashboardShell />, // sidebar + <Outlet /> children: [ { index: true, element: <Home /> }, { path: "projects/:projectId", element: <ProjectFrame />, // header + <Outlet /> children: [ { index: true, element: <ProjectOverview /> }, { path: "tasks/:taskId", element: <TaskScreen /> }, ], }, ], }, ]); ``` ## How paths combine - **Child paths are relative.** `tasks/:taskId` under `projects/:projectId` under `/` matches `/projects/42/tasks/7`. You never repeat the parent's segments. - **Absolute child paths must repeat the parent.** A child written as `/tasks/:taskId` under `projects/:projectId` fails an invariant: an absolute child path must start with the combined path of all its parents. - **A route path may span several segments.** `projects/:projectId` is one route covering two segments; there is no requirement to have a `projects` route with its own element. - **Dynamic segments** start with `:`. Their values are exposed to components through the router's params APIs; declaring them is all the route tree does. ## What renders at each URL | URL | Rendered tree | |---|---| | `/` | `DashboardShell` → `Home` | | `/projects/42` | `DashboardShell` → `ProjectFrame` → `ProjectOverview` | | `/projects/42/tasks/7` | `DashboardShell` → `ProjectFrame` → `TaskScreen` | | `/projects/42/tasks/8` | same components as above, new params | The **index route** (`index: true`) renders into the parent's outlet when the URL is exactly the parent's URL. Without it, `/projects/42` would render `ProjectFrame` with an empty outlet. Index routes cannot have children; the router enforces that with an invariant. ## Why this keeps one sidebar Because the shell is the element of an ancestor route, it matches **every** URL in the tree. Navigating from task 7 to task 8, or from one project to another, keeps `DashboardShell` mounted; only the parts whose route changed are swapped. That is the practical benefit interviewers look for: shared chrome is declared once, in one place, and does not remount on navigation. ## Common mistakes - **Forgetting `<Outlet />` in a parent element.** Children still match, the URL changes and nothing appears. There is no error; the parent simply renders without its child. - **Rendering `<Outlet />` twice.** Both positions show the same child element, which is almost never intended. - **Flattening everything as siblings** (`projects/:projectId`, `projects/:projectId/tasks/:taskId` side by side, each rendering its own copy of the header). It works, but duplicates the frame and remounts it on every switch between them. - **Using a leading slash in a nested child path** without repeating the parent, which trips the absolute-path invariant. - **Expecting the shell to receive the child as a prop.** The child is placed by `<Outlet />`, not passed through props. ## Adding a new screen to the tree Suppose the product asks for a task activity log at `/projects/42/tasks/7/activity`, shown under the task's own header. Working through it: 1. Decide which **layer** it belongs to. It sits inside a task, so it becomes a child of `tasks/:taskId`, not a sibling. 2. Give the task route an element that renders the task header plus an `<Outlet />`, and move the current task body into an **index** child so `/projects/42/tasks/7` still shows it. 3. Add `{ path: "activity", element: <TaskActivity /> }` as the second child, with a relative path. The shell and project frame need no change, because they already wrap every descendant. ## Summary - Mirror the URL's layers as nested routes; each non-leaf element renders `<Outlet />`. - Child paths are relative to their parent; one route can cover several segments. - An index route fills the outlet at the parent's own URL. - Shared layout lives in the ancestor route and stays mounted while descendants change.

  • In React Router v7, what does <Outlet /> render when no child route matches the current URL?
    Nothing. `<Outlet />` renders the matched child route's element, or `null` when there is none. That is why an index route matters: it is the child that matches at the parent's exact URL, so the outlet is not left empty on `/projects/42`.
  • In React Router v7, can a route have children but no element?
    Yes. A route with a `path` and no `element` just adds a path prefix to its children; with no element of its own there is nothing to wrap them, so they render directly into the nearest ancestor's outlet. It is useful for grouping URLs such as `projects` without adding a layout layer.

saying these in an interview costs you the question

  • Child route paths must repeat the parent path with a leading slash
  • A parent without <Outlet /> throws an error when a child matches
  • Each child route re-mounts the shared layout on navigation
  • Nested routes require a separate route object for every URL segment
  • The parent receives the matched child as a children prop
open as a page

In React Router v7, sibling routes "projects/new" and "projects/:projectId" both match /projects/new; which renders, and does declaration order matter?

level: middleimportance: must knowfreq 52%

basics

~20 s

projects/new renders, whatever the order. React Router ranks every candidate route by specificity, so a static segment outranks a dynamic one and a splat ranks lowest. Declaration order only decides between siblings with identical scores.

open as a page

In React Router v7, which URLs do the route paths ":lang?/categories" and "files/*" match, and where may the * appear?

level: middleimportance: should knowfreq 44%

basics

~10 s

:lang?/categories matches /categories and /en/categories, with lang undefined when absent. files/* matches /files and anything below it, like /files/a/b. A * only works as the final segment, directly after a slash.

open as a page

In React Router v7, how does a project layout route share its project with child routes through <Outlet context>, and how far does that value reach?

level: middleimportance: should knowfreq 38%

basics

~20 s

The layout renders <Outlet context={value} /> and child route components call useOutletContext() to read it. The value reaches the route rendered in that outlet and its components, but not grandchild routes behind another bare <Outlet />.

open as a page

In a React Router v7 dashboard, switching from /projects/1/tasks/7 to /projects/1/tasks/8 leaves the task form showing task 7's draft; why, and how do you fix it?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Both URLs match the same tasks/:taskId route, so React Router renders the same element in the same place and React keeps the component instance and its state. Key the form by the task id, or make effects depend on it.

open as a page