In React Router v7, how do you configure /projects/:projectId/tasks/:taskId so every screen renders inside one shared dashboard shell?
answer
- routes inside routes
- children paths without a leading slash
- the parent decides where the child goes
- index route for the bare parent URL
basics
~20 sNest 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 sDeclare 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 linesimport { 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
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.
Explain how paths are joined, the absolute-child-path invariant, multi-segment route paths and which elements render at each URL in the tree.
Show judgment in structuring layers so shared chrome stays mounted, and diagnose an empty outlet or a duplicated header caused by flattened routes.
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