Nested routes and layouts
Routing covered the flat version of the story: one Route per page, each with its own path and element. Real apps quickly outgrow that, because URLs nest. A path like /host/income has two parts, and often the page at the deeper path keeps most of the page at the shallower path: the same navbar, the same section tabs, with new content swapped in below. Nested routes are how React Router expresses that. You nest Route elements inside each other, and the router renders the whole matching chain at once, parents wrapping children.
So "nested routes" means two related things. The URL segments nest, and the UI nests with them: pieces of the page persist while you navigate deeper, and only the inner part changes. The second one is the real payoff.
Nesting routes and rendering children with Outlet
Route works as a self-closing element, and it also works with children. Its children must be other Route elements. When a child route matches, React Router renders the parent's element first, and the parent decides where the child's element appears by rendering an Outlet.
import { BrowserRouter, Routes, Route } from 'react-router-dom'
function App() {
return (
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="vans" element={<Vans />} />
</Route>
</Routes>
</BrowserRouter>
)
}import { Outlet } from 'react-router-dom'
import Header from './Header'
export default function Layout() {
return (
<>
<Header />
<Outlet />
</>
)
}Outlet punches a hole in the parent's element where the matching child renders. It plays the same role the children prop plays in composition: the parent renders shared structure and marks the spot where the variable part goes. The difference is that React Router fills the hole, choosing whichever child route matches the current URL. Without an Outlet, the parent's element takes over the page and the matched child renders nowhere, which is the first thing to check when a nested page comes up blank.
That outer route above is a layout route: it has an element and no path. A pathless route matches every URL its children match, so Layout wraps every page, which is exactly where site-wide chrome like a header and footer belongs. A header rendered next to Routes does the same job while the app has exactly one layout; layout routes are what let a section like /host swap in different chrome without touching the pages underneath. The option to avoid is repeating the header inside every page component.
Layouts with a path
A layout route can also carry a path, which scopes the shared UI to one section of the app. The course's VanLife project adds a /host section with its own secondary nav, so the host pages get a second layout nested inside the first:
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="host" element={<HostLayout />}>
<Route path="income" element={<Income />} />
<Route path="reviews" element={<Reviews />} />
</Route>
</Route>HostLayout renders the host nav plus an Outlet, so at /host/income the router renders Layout, then HostLayout in its outlet, then Income in the host layout's outlet. Three routes match at once, and each parent frames the next. Layouts stack as deep as the design calls for.
Index routes
There is one gap in that setup: HostLayout renders at /host, but its outlet has nothing to show there, because every child adds a segment to the path. The fix is an index route: replace path with the index prop, and that child renders in the parent's outlet at the parent's own path.
<Route index element={<Dashboard />} />Now /host shows the host nav with the dashboard inside it, and /host/income swaps the dashboard for the income page. Think of index as "the default child": what belongs in the outlet before any deeper segment is added. It works at the top level too, where <Route index element={<Home />} /> is the home page inside the root layout, the same route the opening example wrote as path="/".
Relative paths and relative links
Notice the child paths above: income, with no leading slash. A path that starts with / is absolute, measured from the root of the site. A path without one is relative to its parent route, so the nested income means /host/income. This keeps deep route trees maintainable: rename host to admin in one place and every descendant path follows.
Links get the same treatment. A Link rendered inside a routed element resolves relative paths against the route that rendered it, so inside HostLayout the nav can drop the /host prefix entirely:
import { Link, Outlet } from 'react-router-dom'
export default function HostLayout() {
return (
<>
<nav>
<Link to=".">Dashboard</Link>
<Link to="income">Income</Link>
<Link to="reviews">Reviews</Link>
</nav>
<Outlet />
</>
)
}Two special values borrow their meaning from file system paths. to="." links to the current route, which is how the Dashboard link targets /host itself without hard-coding it. to=".." climbs upward, useful for "back to the list" links; it comes with a subtlety, since it climbs one route rather than one URL segment, which starts to matter once a single route owns several segments, and Route params and location puts it to work on a real back button.
NavLink and ancestor matching
Swap those Link elements for NavLink to style the current page, as the course does for the host nav, and ancestor matching comes into play: the router counts every ancestor of the current URL as matched, so the to="." Dashboard link reports active on /host/income and /host/reviews as well. Add end to that one link and it stays unstyled anywhere below /host.
Passing data down with useOutletContext
A layout often owns data its children need. HostVanDetail in the course fetches one van and keeps it in state, then renders a tab nav and an Outlet where the info, pricing, or photos page appears. Props can't reach through an Outlet, since the layout never renders those components directly. React Router's answer is the context prop on Outlet, read on the other side with useOutletContext:
<Outlet context={{ currentVan }} />import { useOutletContext } from 'react-router-dom'
export default function HostVanInfo() {
const { currentVan } = useOutletContext()
return <h4>Name: {currentVan.name}</h4>
}Whatever value you pass to context is what useOutletContext returns in whichever child currently fills the outlet. Passing an object and destructuring on the receiving end is the common shape, and it leaves room to add more values later. If Context is familiar, this will feel familiar too: the outlet acts as a provider scoped to exactly one hole in the page.
To nest or not to nest
Nesting earns its keep when routes share UI. That is the test. The URL alone can tempt you into nesting: /vans and /vans/:id share a segment, so why not share a route? But in VanLife those two pages share nothing visible beyond the site-wide chrome, and nesting them forces an awkward shape: a parent route with a path and no element, which falls back to rendering a bare Outlet, plus an index child to render the list. That is more structure for zero benefit.
When there is shared UI to keep on screen, nest and give the parent a layout element with an Outlet. When the only motivation is avoiding a repeated path segment, two flat routes are the simpler, clearer choice.
Version note
The examples here import from react-router-dom, which works in both v6 and v7; Routing covers what v7 changed about the packages. The nested-route concepts are identical in both versions.
Outlet where the child page should appear. A route with an element and no path is a layout that wraps everything, and an index route is the default child shown at the parent's own address.
If two pages share nothing on screen, there is no need to nest them.
Next up: Route params and location, where a single route serves many pages.

