Nested Routes in React Router: Complete Guide to Layout Patterns
Nested routes are one of the most powerful features in React Router, yet many developers struggle to implement them correctly. They let you render layouts and preserve UI state across route transitions, solving a fundamental problem in single-page applications: sharing navigation and other page-level components across multiple pages without recreating them.
In this guide, you'll learn not just how to use nested routes, but why they matter and how to architect your routing structure for maximum scalability and maintainability.
Table of Contents
- Understanding Nested Routes
- The Outlet Component: Where Child Routes Render
- Basic Nested Route Structure
- Layout Routes vs Child Routes
- Practical Example: Multi-Level Dashboard
- Dynamic Routes with Nesting
- Common Pitfalls and Solutions
- Advanced Patterns
- FAQ
Understanding Nested Routes
A nested route is a route that sits inside another route's component. Rather than rendering completely separate layouts for each page, nested routes allow parent routes to render child routes at a specific location, creating a hierarchical routing structure.
Consider a typical e-commerce dashboard: you have a main app layout with navigation, and inside that layout, you want different content pages like "Products," "Orders," and "Settings." Without nested routes, switching between these pages would unmount your entire navigation bar and re-render it. With nested routes, the navigation stays mounted while only the child content changes.
This is fundamentally different from traditional multi-page applications where each page is entirely separate. Nested routes give you fine-grained control over which parts of your UI unmount and remount during navigation.
Why Nested Routes Matter
The problem nested routes solve is architectural. Consider this scenario without nested routes:
// ❌ Without nesting - navigation recreates on every route change
const router = createBrowserRouter([
{
path: '/',
element: <Dashboard />, // Includes navigation
},
{
path: '/orders',
element: <Orders />, // Includes navigation again
},
{
path: '/settings',
element: <Settings />, // Includes navigation again
},
]);
Each component must include its own navigation. This leads to code duplication, inconsistent state, and performance issues. When you navigate from / to /orders, the entire page unmounts and remounts, including the navigation.
With nested routes, you create a layout hierarchy:
// ✅ With nesting - navigation shared across all routes
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
{
index: true,
element: <Dashboard />, // Just the content
},
{
path: 'orders',
element: <Orders />, // Just the content
},
{
path: 'settings',
element: <Settings />, // Just the content
},
],
},
]);
The navigation lives once in AppLayout, and child routes inject their content without remounting the parent.
The Outlet Component: Where Child Routes Render
The critical piece that makes nested routes work is React Router's <Outlet /> component. This is a placeholder that tells React Router where to render the matched child route component.
Without <Outlet />, child routes have nowhere to render, and you'll see nothing on your page.
Basic Outlet Usage
import { Outlet, Link } from 'react-router-dom';
export function AppLayout() {
return (
<>
<header>
<nav>
<Link to="/">Dashboard</Link>
<Link to="orders">Orders</Link>
<Link to="settings">Settings</Link>
</nav>
</header>
<main>
<Outlet /> {/* Child routes render here */}
</main>
</>
);
}
import { Outlet, Link } from 'react-router-dom';
export function AppLayout() {
return (
<>
<header>
<nav>
<Link to="/">Dashboard</Link>
<Link to="orders">Orders</Link>
<Link to="settings">Settings</Link>
</nav>
</header>
<main>
<Outlet /> {/* Child routes render here */}
</main>
</>
);
}
When you navigate to /, the <Dashboard /> component renders where <Outlet /> is placed. When you navigate to /orders, the <Orders /> component renders in the same location, but the <AppLayout /> component itself never unmounts.
Accessing Outlet Context
React Router allows you to pass data to child routes through Outlet context, useful for sharing application state without prop drilling:
import { Outlet, createContext, useContext } from 'react-router-dom';
interface LayoutContextType {
sidebarCollapsed: boolean;
toggleSidebar: () => void;
}
const LayoutContext = createContext<LayoutContextType | null>(null);
export function AppLayout() {
const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false);
return (
<LayoutContext.Provider
value={{
sidebarCollapsed,
toggleSidebar: () => setSidebarCollapsed(!sidebarCollapsed),
}}
>
<div className="flex">
<aside className={sidebarCollapsed ? 'w-16' : 'w-64'}>
{/* Sidebar content */}
</aside>
<main className="flex-1">
<Outlet context={{ sidebarCollapsed }} />
</main>
</div>
</LayoutContext.Provider>
);
}
// In a child route component
import { useOutletContext } from 'react-router-dom';
export function Orders() {
const { sidebarCollapsed } = useOutletContext<{ sidebarCollapsed: boolean }>();
return <div>Orders content - Sidebar collapsed: {sidebarCollapsed}</div>;
}
import { Outlet, createContext, useContext } from 'react-router-dom';
const LayoutContext = createContext(null);
export function AppLayout() {
const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false);
return (
<LayoutContext.Provider
value={{
sidebarCollapsed,
toggleSidebar: () => setSidebarCollapsed(!sidebarCollapsed),
}}
>
<div className="flex">
<aside className={sidebarCollapsed ? 'w-16' : 'w-64'}>
{/* Sidebar content */}
</aside>
<main className="flex-1">
<Outlet context={{ sidebarCollapsed }} />
</main>
</div>
</LayoutContext.Provider>
);
}
// In a child route component
import { useOutletContext } from 'react-router-dom';
export function Orders() {
const { sidebarCollapsed } = useOutletContext();
return <div>Orders content - Sidebar collapsed: {sidebarCollapsed}</div>;
}
Basic Nested Route Structure
Let's build a complete example from scratch. Here's the simplest nested route setup:
Step 1: Define Your Route Configuration
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { AppLayout } from './layouts/AppLayout';
import { Dashboard } from './pages/Dashboard';
import { Orders } from './pages/Orders';
import { Settings } from './pages/Settings';
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
{
index: true, // This route is active when parent path matches exactly
element: <Dashboard />,
},
{
path: 'orders',
element: <Orders />,
},
{
path: 'settings',
element: <Settings />,
},
],
},
]);
export function App() {
return <RouterProvider router={router} />;
}
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { AppLayout } from './layouts/AppLayout';
import { Dashboard } from './pages/Dashboard';
import { Orders } from './pages/Orders';
import { Settings } from './pages/Settings';
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
{
index: true,
element: <Dashboard />,
},
{
path: 'orders',
element: <Orders />,
},
{
path: 'settings',
element: <Settings />,
},
],
},
]);
export function App() {
return <RouterProvider router={router} />;
}
Step 2: Create the Layout Component
import { Outlet, NavLink } from 'react-router-dom';
export function AppLayout() {
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow">
<nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ul className="flex space-x-8">
<li>
<NavLink
to="/"
className={({ isActive }) =>
`py-2 px-1 border-b-2 font-medium text-sm ${
isActive
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`
}
>
Dashboard
</NavLink>
</li>
<li>
<NavLink
to="orders"
className={({ isActive }) =>
`py-2 px-1 border-b-2 font-medium text-sm ${
isActive
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`
}
>
Orders
</NavLink>
</li>
<li>
<NavLink
to="settings"
className={({ isActive }) =>
`py-2 px-1 border-b-2 font-medium text-sm ${
isActive
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`
}
>
Settings
</NavLink>
</li>
</ul>
</nav>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Outlet />
</main>
</div>
);
}
import { Outlet, NavLink } from 'react-router-dom';
export function AppLayout() {
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow">
<nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ul className="flex space-x-8">
<li>
<NavLink
to="/"
className={({ isActive }) =>
`py-2 px-1 border-b-2 font-medium text-sm ${
isActive
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`
}
>
Dashboard
</NavLink>
</li>
<li>
<NavLink
to="orders"
className={({ isActive }) =>
`py-2 px-1 border-b-2 font-medium text-sm ${
isActive
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`
}
>
Orders
</NavLink>
</li>
<li>
<NavLink
to="settings"
className={({ isActive }) =>
`py-2 px-1 border-b-2 font-medium text-sm ${
isActive
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`
}
>
Settings
</NavLink>
</li>
</ul>
</nav>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Outlet />
</main>
</div>
);
}
Notice the key points:
- The layout component is independent of route content
<NavLink>withclassNamefunction handles active state styling<Outlet />is the placeholder for child route content- Navigation stays mounted while child content changes
Layout Routes vs Child Routes
It's important to understand the terminology:
Layout Routes (or Wrapper Routes): These are parent routes that define shared UI, like headers, sidebars, and footers. They contain <Outlet /> to render child content. Example: AppLayout.
Child Routes (or Nested Routes): These are routes nested inside a layout route's children array. They represent distinct pages or sections within the layout. Example: Dashboard, Orders, Settings.
A layout route must contain an <Outlet /> component, or child routes will have nowhere to render and won't display anything.
Practical Example: Multi-Level Dashboard
Let's build a more realistic example: a customer relationship management (CRM) dashboard with multiple nesting levels.
Route Structure
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { AppLayout } from './layouts/AppLayout';
import { Dashboard } from './pages/Dashboard';
import { CustomersLayout } from './layouts/CustomersLayout';
import { CustomersList } from './pages/CustomersList';
import { CustomerDetail } from './pages/CustomerDetail';
import { CustomerProfile } from './pages/CustomerProfile';
import { CustomerHistory } from './pages/CustomerHistory';
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
{
index: true,
element: <Dashboard />,
},
{
path: 'customers',
element: <CustomersLayout />,
children: [
{
index: true,
element: <CustomersList />,
},
{
path: ':customerId',
element: <CustomerDetail />,
children: [
{
index: true,
element: <CustomerProfile />,
},
{
path: 'history',
element: <CustomerHistory />,
},
],
},
],
},
],
},
]);
export function App() {
return <RouterProvider router={router} />;
}
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { AppLayout } from './layouts/AppLayout';
import { Dashboard } from './pages/Dashboard';
import { CustomersLayout } from './layouts/CustomersLayout';
import { CustomersList } from './pages/CustomersList';
import { CustomerDetail } from './pages/CustomerDetail';
import { CustomerProfile } from './pages/CustomerProfile';
import { CustomerHistory } from './pages/CustomerHistory';
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
{
index: true,
element: <Dashboard />,
},
{
path: 'customers',
element: <CustomersLayout />,
children: [
{
index: true,
element: <CustomersList />,
},
{
path: ':customerId',
element: <CustomerDetail />,
children: [
{
index: true,
element: <CustomerProfile />,
},
{
path: 'history',
element: <CustomerHistory />,
},
],
},
],
},
],
},
]);
export function App() {
return <RouterProvider router={router} />;
}
Layout Components
The CustomersLayout acts as a layout for customer-specific routes:
import { Outlet } from 'react-router-dom';
export function CustomersLayout() {
return (
<div className="grid grid-cols-4 gap-6">
<aside className="col-span-1 bg-white p-4 rounded shadow">
<h3 className="font-bold mb-4">Customers</h3>
{/* Sidebar content - might fetch customer list */}
</aside>
<main className="col-span-3">
<Outlet />
</main>
</div>
);
}
import { Outlet } from 'react-router-dom';
export function CustomersLayout() {
return (
<div className="grid grid-cols-4 gap-6">
<aside className="col-span-1 bg-white p-4 rounded shadow">
<h3 className="font-bold mb-4">Customers</h3>
{/* Sidebar content - might fetch customer list */}
</aside>
<main className="col-span-3">
<Outlet />
</main>
</div>
);
}
The CustomerDetail layout renders customer-specific navigation:
import { Outlet, NavLink, useParams } from 'react-router-dom';
export function CustomerDetail() {
const { customerId } = useParams<{ customerId: string }>();
return (
<>
<h2 className="text-2xl font-bold mb-4">Customer #{customerId}</h2>
<div className="border-b mb-4">
<nav className="flex space-x-4">
<NavLink
to=""
className={({ isActive }) => isActive ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-600'}
>
Profile
</NavLink>
<NavLink
to="history"
className={({ isActive }) => isActive ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-600'}
>
History
</NavLink>
</nav>
</div>
<Outlet />
</>
);
}
import { Outlet, NavLink, useParams } from 'react-router-dom';
export function CustomerDetail() {
const { customerId } = useParams();
return (
<>
<h2 className="text-2xl font-bold mb-4">Customer #{customerId}</h2>
<div className="border-b mb-4">
<nav className="flex space-x-4">
<NavLink
to=""
className={({ isActive }) => isActive ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-600'}
>
Profile
</NavLink>
<NavLink
to="history"
className={({ isActive }) => isActive ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-600'}
>
History
</NavLink>
</nav>
</div>
<Outlet />
</>
);
}
This three-level nesting structure means:
/customers→ ShowsCustomersLayoutwithCustomersList/customers/123→ ShowsCustomersListin sidebar,CustomerDetailwithCustomerProfilein main area/customers/123/history→ ShowsCustomersListin sidebar,CustomerDetailwithCustomerHistoryin main area
Dynamic Routes with Nesting
Dynamic routes (using path parameters like :id) work seamlessly with nesting. The pattern above demonstrates this: :customerId is a dynamic segment that's available to child routes through useParams().
Accessing Route Parameters in Nested Layouts
import { Outlet, useParams } from 'react-router-dom';
export function ProductLayout() {
const { productId } = useParams<{ productId: string }>();
return (
<div>
<header>
{/* Product ID is available here */}
<h1>Product {productId}</h1>
</header>
<Outlet />
</div>
);
}
import { Outlet, useParams } from 'react-router-dom';
export function ProductLayout() {
const { productId } = useParams();
return (
<div>
<header>
<h1>Product {productId}</h1>
</header>
<Outlet />
</div>
);
}
Parameters defined in parent routes are accessible to all descendant routes.
Common Pitfalls and Solutions
Pitfall 1: Forgetting the <Outlet />
Problem: Child routes don't render.
// ❌ WRONG - no Outlet
export function AppLayout() {
return (
<header>Navigation</header>
// Child routes disappear!
);
}
// ✅ CORRECT
export function AppLayout() {
return (
<>
<header>Navigation</header>
<Outlet />
</>
);
}
Pitfall 2: Incorrect Relative Path Navigation
Problem: Navigation links break with incorrect relative paths.
// ❌ WRONG - NavLink to an absolute path won't work within nested children
export function CustomersLayout() {
return (
<nav>
<Link to="/customers/settings">Settings</Link> {/* Breaks in nested routes */}
</nav>
);
}
// ✅ CORRECT - use relative paths
export function CustomersLayout() {
return (
<nav>
<Link to="settings">Settings</Link> {/* Relative to current path */}
</nav>
);
}
Pitfall 3: Not Using index for Default Routes
Problem: No default content when visiting parent path.
// ❌ WRONG - no route for "/" when it's the parent
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
{
path: 'orders', // This covers /orders but what about /?
element: <Orders />,
},
],
},
]);
// ✅ CORRECT - use index for the default child route
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
{
index: true, // This is active when path is exactly /
element: <Dashboard />,
},
{
path: 'orders',
element: <Orders />,
},
],
},
]);
Advanced Patterns
Lazy Loading Nested Routes
For performance optimization, load route-specific components only when needed:
import { lazy, Suspense } from 'react';
import { createBrowserRouter } from 'react-router-dom';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Orders = lazy(() => import('./pages/Orders'));
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
{
index: true,
element: (
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
),
},
{
path: 'orders',
element: (
<Suspense fallback={<div>Loading orders...</div>}>
<Orders />
</Suspense>
),
},
],
},
]);
import { lazy, Suspense } from 'react';
import { createBrowserRouter } from 'react-router-dom';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Orders = lazy(() => import('./pages/Orders'));
const router = createBrowserRouter([
{
path: '/',
element: <AppLayout />,
children: [
{
index: true,
element: (
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
),
},
{
path: 'orders',
element: (
<Suspense fallback={<div>Loading orders...</div>}>
<Orders />
</Suspense>
),
},
],
},
]);
Error Boundaries in Nested Routes
Handle errors at different nesting levels:
import { Outlet } from 'react-router-dom';
export function ErrorBoundaryLayout() {
return (
<div>
<header>App Header</header>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Outlet />
</ErrorBoundary>
</div>
);
}
import { Outlet } from 'react-router-dom';
export function ErrorBoundaryLayout() {
return (
<div>
<header>App Header</header>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Outlet />
</ErrorBoundary>
</div>
);
}
Conditional Nested Routes
Show different child routes based on user role or state:
import { Outlet, Navigate } from 'react-router-dom';
import { useAuthStore } from './hooks/useAuthStore';
export function ProtectedLayout() {
const { user } = useAuthStore();
if (!user) {
return <Navigate to="/login" />;
}
return (
<>
<header>Welcome {user.name}</header>
<Outlet context={{ user }} />
</>
);
}
import { Outlet, Navigate } from 'react-router-dom';
import { useAuthStore } from './hooks/useAuthStore';
export function ProtectedLayout() {
const { user } = useAuthStore();
if (!user) {
return <Navigate to="/login" />;
}
return (
<>
<header>Welcome {user.name}</header>
<Outlet context={{ user }} />
</>
);
}
FAQ
Q: How many levels of nesting should I use?
A: Use nesting when it reflects your actual UI hierarchy. Three to four levels is typical for most applications. Beyond that, your route structure becomes hard to understand. If you find yourself nesting more than four levels, consider refactoring your components or splitting routes into separate feature modules.
Q: Should I use nested routes or nested components?
A: Use nested routes when different content paths should have different URLs. Use nested components for UI composition without URL changes. Don't put everything in routes—use routes for pages and components for reusable pieces of the UI.
Q: How do I pass data between layout and child routes?
A: Use Outlet context (shown above) or a state management solution like Context API with a provider wrapping your layout. Avoid prop drilling by keeping layout-level state in the layout component and accessing it via context in child routes.
Q: What's the difference between path: '' and index: true?
A: index: true is for the default child route when the parent path matches exactly. path: '' is for a route with an empty path segment (usually combined with a parent that has a dynamic segment). Use index: true for most cases.
Q: Can I have multiple Outlet components in one layout?
A: Yes! This is useful for multi-panel layouts. Each <Outlet /> can render different route content, but you need to configure routes differently (use outlet property in route definition). This is an advanced pattern—most applications only need one <Outlet /> per layout.
Q: How do nested routes affect performance?
A: Nested routes can improve performance because parent layout components don't unmount during navigation. This preserves component state and DOM elements. However, make sure each layout component is optimized—heavy computations in a layout component run for every child route change.
Key Takeaways
Nested routes are essential for building scalable, maintainable React applications. They solve the fundamental problem of sharing UI across multiple pages without recreating layout components on every navigation.
Remember these core principles:
- Every layout route must have an
<Outlet />or child routes won't render - Use
index: truefor default child routes - Leverage
NavLinkwith dynamicclassNamefor active link styling - Use
useParams()to access parent route parameters in child components - Consider multi-level nesting for complex hierarchical UIs
Master nested routes and you'll write cleaner, more efficient routing code that scales with your application.
Questions or insights? Share your nested routes patterns in the comments below. What layout structures have you found most effective in your applications?
Google AdSense Placeholder
CONTENT SLOT