πͺ Nested Routes and Layout Patterns
Dashboards, settings panels, and admin consoles all share a shell β a header and sidebar that stay put while the inner content changes. Nested routes let your URL structure mirror that visual hierarchy, so shared layout is written once and never re-mounts as users move around.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain how a parent route renders shared layout and where
<Outlet>injects children - Use index routes to provide default content for a parent's exact path
- Build pathless layout routes to wrap groups of pages in a common shell
- Compose multiple and deeply nested layouts for dashboards and admin sections
- Share data down the tree with
useOutletContextand relative links
Estimated Time: 40β50 minutes β’ Difficulty: Intermediate
Hands-on: Build a dashboard shell with a persistent sidebar and swappable Overview / Profile / Settings panels.
In This Lesson
Why Nest Routes?
Nested routes let you define routes hierarchically: a parent route renders a layout, and its child routes render inside that layout. The parent stays mounted while children swap. This gives you consistent shells for related sections, no duplicated headers or sidebars, a routing tree that mirrors your UI, and easy data-sharing between parent and child.
π’ Analogy β an office building. The building (parent route) has one lobby, one elevator bank, one security desk. Each floor (child route) reuses those without rebuilding them; you only change the office you walk into. Moving between rooms on a floor doesn't send you back to the street. Nested routes keep the "building" mounted and just change the "room."
The Dashboard route owns the sidebar and a content slot. As the URL moves between /dashboard, /dashboard/profile, and /dashboard/settings, only the slot changes β the sidebar never re-renders from scratch.
The Outlet Component
<Outlet> is the key to nesting. It's a placeholder the parent renders to say "child routes go here." You nest <Route> elements inside a parent <Route>, and their elements appear wherever the parent placed its <Outlet>.
import { BrowserRouter, Routes, Route, Outlet, Link } from 'react-router-dom';
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
{/* Parent route renders the Dashboard layout */}
<Route path="/dashboard" element={<Dashboard />}>
{/* Child routes render inside Dashboard's <Outlet /> */}
<Route index element={<DashboardOverview />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</BrowserRouter>
);
}
function Dashboard() {
return (
<div className="dashboard">
<aside className="sidebar">
<h2>Dashboard</h2>
<nav>
<Link to="/dashboard">Overview</Link>
<Link to="/dashboard/profile">Profile</Link>
<Link to="/dashboard/settings">Settings</Link>
</nav>
</aside>
<main className="content">
<Outlet /> {/* <-- child route renders here */}
</main>
</div>
);
}
Key points from this example:
/dashboard/profilerendersProfileinside Dashboard's<Outlet>.- Child paths are relative β
profilebecomes/dashboard/profileautomatically. - The sidebar stays mounted across all three child views; only the content slot changes.
Relative links keep nesting maintainable
Inside a nested route you can use relative links so navigation doesn't hard-code the parent path. Change the parent's path once and every relative link follows:
function Dashboard() {
return (
<div className="dashboard">
<nav>
<Link to=".">Overview</Link> {/* current route */}
<Link to="profile">Profile</Link> {/* child of current */}
<Link to="settings">Settings</Link>
<Link to="..">Back up</Link> {/* parent route */}
</nav>
<Outlet />
</div>
);
}
π How nested paths combine
| Parent path | Child path | Full URL |
|---|---|---|
| /dashboard | profile | /dashboard/profile |
| /users/:userId | posts | /users/:userId/posts |
| /app | settings/notifications | /app/settings/notifications |
Index Routes
What should render at exactly /dashboard, before the user picks Profile or Settings? Without help, the parent renders but its <Outlet> is empty. An index route fills that gap: it's the default child for the parent's exact path.
<Route path="/dashboard" element={<Dashboard />}>
{/* Renders at exactly /dashboard */}
<Route index element={<DashboardOverview />} />
{/* Render at their own paths */}
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
β οΈ An index route has no path
Use the index prop instead of path="/". An index route represents the parent's own URL, so giving it a path is a mistake the compiler won't catch β it just won't match where you expect.
Index routes nest, too. Each layout level can have its own default view:
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<DashboardOverview />} />
<Route path="settings" element={<Settings />}>
{/* Renders at /dashboard/settings */}
<Route index element={<GeneralSettings />} />
<Route path="account" element={<AccountSettings />} />
<Route path="notifications" element={<NotificationSettings />} />
</Route>
</Route>
π‘ Real example β a shop's category tree
Index routes make e-commerce browsing feel natural: the shop home shows featured items, each category shows all its products, and subcategories drill in β all while the shop chrome stays put.
<Route path="/shop" element={<Shop />}>
<Route index element={<FeaturedProducts />} /> {/* /shop */}
<Route path="clothing" element={<Clothing />}>
<Route index element={<AllClothing />} /> {/* /shop/clothing */}
<Route path="mens" element={<Mens />} />
<Route path="womens" element={<Womens />} />
</Route>
</Route>
Layout Route Patterns
A powerful trick: a parent route doesn't need a path at all. A pathless layout route exists purely to wrap a group of children in shared UI, without adding a segment to the URL.
function MainLayout() {
return (
<div>
<SiteHeader />
<main><Outlet /></main>
<SiteFooter />
</div>
);
}
<Routes>
{/* No path β just a shared shell for these pages */}
<Route element={<MainLayout />}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Route>
</Routes>
Because MainLayout has no path, its children keep their own top-level URLs (/, /about, /contact) while all sharing the same header and footer.
Different layouts for different sections
Combine several layout routes to give public pages, the user dashboard, and the admin console each their own chrome:
<Routes>
{/* Public pages */}
<Route element={<MainLayout />}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Route>
{/* Signed-in dashboard */}
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
{/* Admin console */}
<Route path="/admin" element={<AdminLayout />}>
<Route index element={<AdminHome />} />
<Route path="users" element={<UserManagement />} />
</Route>
</Routes>
β Guard once, protect the whole section
This is the payoff promised in the last lesson: put your auth check inside DashboardLayout (redirect with <Navigate> when there's no user). Every child route inherits the protection β no need to wrap each page individually.
function DashboardLayout() {
const { user } = useAuth();
if (!user) return <Navigate to="/login" replace />;
return (
<div className="dashboard">
<DashboardSidebar />
<main><Outlet /></main>
</div>
);
}
Deeply Nested Layouts
Layouts compose. A section inside the dashboard can have its own layout β a header and tab bar β nested within the dashboard's shell. This models complex apps cleanly: each level adds exactly the chrome it owns.
function ProjectsLayout() {
return (
<div className="projects">
<header>
<h1>Projects</h1>
<Link to="new" className="button">New Project</Link>
</header>
<nav className="tabs">
<NavLink to="." end>All</NavLink>
<NavLink to="active">Active</NavLink>
<NavLink to="archived">Archived</NavLink>
</nav>
<Outlet />
</div>
);
}
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardOverview />} />
{/* Projects section adds its own layout inside the dashboard */}
<Route path="projects" element={<ProjectsLayout />}>
<Route index element={<AllProjects />} />
<Route path="active" element={<ActiveProjects />} />
<Route path="archived" element={<ArchivedProjects />} />
<Route path=":projectId" element={<ProjectDetails />} />
<Route path="new" element={<NewProject />} />
</Route>
</Route>
Navigating to /dashboard/projects/active now renders three nested shells at once:
DashboardLayoutβ global header + sidebarProjectsLayoutβ projects header + tab barActiveProjectsβ the actual list
<Outlet> renders the next level inward. Only the innermost box changes as the user switches tabs; the outer shells stay mounted.Hands-on Exercise
ποΈ Build a dashboard shell
Objective: practice Outlet, index routes, and shared context.
Requirements:
- Create a
/dashboardparent route rendering a persistent sidebar with links to Overview, Profile, and Settings. - Add an index route so
/dashboardshows the Overview by default. - Add
profileandsettingschild routes that render in the parent's<Outlet>. - Store a
userNamein the layout and pass it to all children via<Outlet context>; display it on the Overview. - Use relative links (
to=".",to="profile") so nothing hard-codes/dashboard.
π‘ Hint
The parent renders the chrome and <Outlet context={{ userName }} />. Children call const { userName } = useOutletContext(). Remember the index child uses <Route index element={β¦} /> with no path.
β Solution sketch
import { BrowserRouter, Routes, Route, Outlet, NavLink, useOutletContext } from 'react-router-dom';
function DashboardLayout() {
const userName = 'Ray';
const cls = ({ isActive }) => (isActive ? 'active' : undefined);
return (
<div className="dashboard">
<aside>
<NavLink to="." end className={cls}>Overview</NavLink>
<NavLink to="profile" className={cls}>Profile</NavLink>
<NavLink to="settings" className={cls}>Settings</NavLink>
</aside>
<main>
<Outlet context={{ userName }} />
</main>
</div>
);
}
function Overview() {
const { userName } = useOutletContext();
return <h1>Welcome back, {userName}!</h1>;
}
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Overview />} />
<Route path="profile" element={<h1>Profile</h1>} />
<Route path="settings" element={<h1>Settings</h1>} />
</Route>
</Routes>
</BrowserRouter>
);
}
π― Quick Quiz
Question 1: What is the role of the <Outlet> component?
Question 2: You want content to appear at exactly /dashboard (the parent's own path). Which route do you add?
Question 3: A parent route fetches a project and needs to share it with its child tab routes without prop-drilling. What's the idiomatic React Router tool?
Best Practices
β Do
- Let your route tree mirror your visual hierarchy β one layout route per shared shell.
- Use pathless layout routes to group pages without polluting the URL.
- Give every layout an index route so its bare path is never a blank
<Outlet>. - Guard a section once at its layout route; share fetched data with
useOutletContext. - Prefer relative links (
.,..,child) so renaming a parent path doesn't break navigation.
β Don't
- Don't forget the
<Outlet>in a layout β children silently won't render. - Don't give an index route a
path; use theindexprop. - Don't nest so deeply that the tree becomes hard to follow β flatten sections that don't share chrome.
- Don't reach for
useOutletContextwhen data is needed app-wide; a proper context or the data router fits better.
Summary & Quiz
π Key Takeaways
- Nested routes let a parent render shared layout while children swap inside its
<Outlet>; the parent never re-mounts. - Index routes (
<Route index>, no path) supply default content for a parent's exact URL. - Pathless layout routes wrap groups of pages in common chrome without adding a URL segment β the clean place for a section-wide auth guard.
- Layouts compose to any depth; each
<Outlet>renders the next level inward. useOutletContextpasses parent-loaded data down to children without prop-drilling.
π Further Reading
π What's Next?
You've finished the routing mini-series. Next up: Memoization with React.memo, where you'll shift from navigation to performance β stopping needless re-renders so your nicely structured app stays fast.
π Excellent work!
You can now architect dashboards and admin consoles whose URLs and layouts line up perfectly. That's a hallmark of a well-built React app.