Nested & Layout Routes
Most real applications share visual chrome across many pages: a top navigation bar, a sidebar, a footer, maybe a dashboard shell. Nested routes let you express that hierarchy directly in your route configuration so a parent route renders the shared layout once, and child routes render only the part that changes. The glue that makes this work is the <Outlet /> component, which marks where a parent should render its matched child. This keeps your component tree DRY, your URLs meaningful, and your layouts stable as users navigate between sibling pages.
How nesting works
In React Router, routes form a tree. When a URL matches a deeply nested route, every parent route along the path is rendered too, from the outside in. A parent route component renders its own UI plus an <Outlet /> placeholder; React Router substitutes the active child element into that slot. Because parents stay mounted while only the child swaps, shared state and DOM (scroll position in a sidebar, an open menu) survive navigation between siblings.
// main.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import App from "./App.jsx";
createRoot(document.getElementById("root")).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);
Defining a layout route
A layout route is simply a parent <Route> whose element contains an <Outlet />. Children declared inside it inherit that layout. Notice the child paths are written relative to the parent — they do not begin with a slash.
// App.jsx
import { Routes, Route } from "react-router-dom";
import RootLayout from "./layouts/RootLayout.jsx";
import Home from "./pages/Home.jsx";
import Dashboard from "./pages/Dashboard.jsx";
import Settings from "./pages/Settings.jsx";
export default function App() {
return (
<Routes>
<Route path="/" element={<RootLayout />}>
<Route index element={<Home />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
);
}
The RootLayout renders shared chrome and drops an <Outlet /> where the page content belongs.
// layouts/RootLayout.jsx
import { NavLink, Outlet } from "react-router-dom";
export default function RootLayout() {
return (
<div className="app-shell">
<header>
<nav>
<NavLink to="/">Home</NavLink>
<NavLink to="/dashboard">Dashboard</NavLink>
<NavLink to="/settings">Settings</NavLink>
</nav>
</header>
<main>
<Outlet />
</main>
<footer>© 2026 DevCraftly</footer>
</div>
);
}
Now /dashboard renders the header, nav, footer, and the Dashboard page inside <main>. The shell never re-mounts when you move to /settings.
Index routes
An index route is the default child shown when the parent’s path matches exactly. It uses the index prop instead of a path. In the example above, visiting / renders RootLayout with Home in the outlet. Index routes are how you give a layout a meaningful landing page without inventing an extra URL segment.
An index route cannot have children or its own
path. Think of it as answering the question, “what should I show when the parent is matched but no child segment is present?”
Deeper nesting and relative paths
Layouts compose. A section like a settings area can have its own sub-layout with its own outlet, nested inside the root layout.
// App.jsx (expanded)
import { Routes, Route } from "react-router-dom";
import RootLayout from "./layouts/RootLayout.jsx";
import SettingsLayout from "./layouts/SettingsLayout.jsx";
import Home from "./pages/Home.jsx";
import Profile from "./pages/Profile.jsx";
import Billing from "./pages/Billing.jsx";
export default function App() {
return (
<Routes>
<Route path="/" element={<RootLayout />}>
<Route index element={<Home />} />
<Route path="settings" element={<SettingsLayout />}>
<Route index element={<Profile />} />
<Route path="billing" element={<Billing />} />
</Route>
</Route>
</Routes>
);
}
// layouts/SettingsLayout.jsx
import { NavLink, Outlet } from "react-router-dom";
export default function SettingsLayout() {
return (
<section className="settings">
<aside>
{/* Relative links: resolved against the current route */}
<NavLink to=".">Profile</NavLink>
<NavLink to="billing">Billing</NavLink>
</aside>
<div className="settings-content">
<Outlet />
</div>
</section>
);
}
Here /settings shows the profile (the index route) and /settings/billing swaps only the inner content. Relative links matter: to="billing" resolves to /settings/billing because it is relative to the current route. A leading slash (to="/billing") would jump to the app root instead.
| URL | Layouts rendered | Page in inner outlet |
|---|---|---|
/ | RootLayout | Home (index) |
/settings | RootLayout → SettingsLayout | Profile (index) |
/settings/billing | RootLayout → SettingsLayout | Billing |
Pathless layout routes
Sometimes you want to share a layout across some routes without adding a URL segment. Omit the path and the route becomes pathless — it groups children under a shared element while keeping their URLs flat.
<Routes>
<Route element={<MarketingLayout />}>
<Route path="/" element={<Home />} />
<Route path="/pricing" element={<Pricing />} />
</Route>
<Route element={<AppLayout />}>
<Route path="/dashboard" element={<Dashboard />} />
</Route>
</Routes>
Both / and /pricing use MarketingLayout, while /dashboard uses a different shell — all without nesting their URLs.
Best practices
- Put one
<Outlet />in every layout component, and only one per layout level. - Write child
pathvalues relative (no leading slash) so the tree reflects the URL structure. - Use
indexroutes to give layouts a sensible default page instead of redirecting. - Reach for pathless layout routes to share chrome without polluting the URL.
- Keep layouts presentational; load data per page or with route loaders rather than in the shell.
- Use
<NavLink>in shared navigation so the active page is styled automatically.