Real applications are not a flat list of pages: a settings area has a shared header and a tab strip around a changing panel. Nest the <Route> elements and the URL segments nest with them — the parent renders its layout and puts <Outlet /> wherever the matched child belongs. React Router 139,386 renders every matched route in the chain, parent first, so a deep URL mounts the whole stack at once and only the changed leaf re-renders next time.
Two extras make nesting comfortable: a child <Route index> fills the outlet at the parent's exact URL — the "nothing selected yet" panel — and a link written without a leading slash resolves against the route that rendered it, so the parent never has to know its own prefix.
<style>body{font:15px/1.6 system-ui;margin:12px}a{margin-right:12px}
.box{border:1px solid #90a4ae;padding:8px 12px;border-radius:6px;margin-top:8px}</style>
<div id="root"></div>
<script type="importmap">{"imports":{"react":"https://esm.sh/react@19.3.0",
"react-dom/client":"https://esm.sh/react-dom@19.3.0/client?external=react",
"react-router":"https://esm.sh/react-router@8.4.0?external=react"}}</script>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
<script type="text/babel" data-type="module">
import React from 'react';
import { createRoot } from 'react-dom/client';
import { MemoryRouter, Routes, Route, Link, Outlet, useParams,
useLocation } from 'react-router';
function Topics() { // the parent, always on screen
return <div className="box"><b>Topics</b> (parent route)
<div><Link to="jsx">jsx</Link><Link to="hooks">hooks</Link></div>
<Outlet /> {/* the matched child lands here */}
</div>;
}
function Topic() { return <div className="box">Topic: {useParams().topicId}</div>; }
function Url() { return <code>{useLocation().pathname}</code>; }
function App() {
return <MemoryRouter initialEntries={['/topics']}>
<div className="box">URL: <Url /></div>
<Routes>
<Route path="/topics" element={<Topics />}>
<Route index element={<div className="box">Pick a topic.</div>} />
<Route path=":topicId" element={<Topic />} />
</Route>
</Routes>
</MemoryRouter>;
}
createRoot(document.getElementById('root')).render(<App />);
setTimeout(() => document.querySelectorAll('a')[1].click(), 300); // click "hooks"
</script>
A parent with no path of its own is a layout route: it groups children under a shared element without adding a URL segment, which is how a guard (Protected Routes) is expressed. Prefer the outlet to repeating a wrapper in every child — it is what lets the parent keep scroll position, open menus and local state across child navigations.