Understanding Nested Routes
Nested routes are a powerful feature of React Router that allow you to define routes hierarchically, where a parent route serves as a layout or container for its child routes. This approach enables you to:
- Create consistent layouts for related sections of your app
- Avoid duplicating common UI elements across similar pages
- Organize your routing structure to match your UI/UX design
- Share data and state between parent and child routes
- Simplify navigation between related views
In this diagram, the Dashboard route acts as a container with its own layout (sidebar), and it renders different child routes (Overview, Profile, Settings) within its content area. The parent Dashboard component stays mounted while the child routes change.
Real-World Analogy: Office Building Layout
Nested routes are like the physical layout of an office building:
- The building itself (parent route) has common elements like the entrance, elevators, and security desk
- Different floors (child routes) share these common elements but contain different departments
- Each floor might have its own reception area (layout) that remains consistent for all rooms on that floor
- Moving between rooms on the same floor (navigating between sibling routes) doesn't require going back to the main entrance
Just as you wouldn't rebuild the entire building structure when moving between offices, nested routes allow you to keep common UI elements mounted while swapping out the specific content.
Setting Up Nested Routes
In React Router 6, nested routes can be defined either through component nesting in JSX or through path nesting in route configuration objects.
Using the Outlet Component
The Outlet component is the key to nested routes. It acts as a placeholder where child routes will be rendered within the parent component.
// Basic nested routes example
import { BrowserRouter, Routes, Route, Outlet, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
{/* Parent route */}
<Route path="/dashboard" element={<Dashboard />}>
{/* Child routes */}
<Route index element={<DashboardOverview />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</BrowserRouter>
);
}
// Parent component with Outlet
function Dashboard() {
return (
<div className="dashboard-container">
<div className="dashboard-sidebar">
<h2>Dashboard</h2>
<nav>
<ul>
<li><Link to="/dashboard">Overview</Link></li>
<li><Link to="/dashboard/profile">Profile</Link></li>
<li><Link to="/dashboard/settings">Settings</Link></li>
</ul>
</nav>
</div>
<div className="dashboard-content">
{/* Child routes render here */}
<Outlet />
</div>
</div>
);
}
// Child components
function DashboardOverview() {
return <h2>Dashboard Overview</h2>;
}
function Profile() {
return <h2>User Profile</h2>;
}
function Settings() {
return <h2>Settings</h2>;
}
function Home() {
return (
<div>
<h1>Home Page</h1>
<Link to="/dashboard">Go to Dashboard</Link>
</div>
);
}
In this example:
- The
/dashboardroute renders theDashboardcomponent - Child routes like
/dashboard/profilerender inside the<Outlet />in the Dashboard component - The
indexroute renders when the path is exactly/dashboardwith no further segments - The Dashboard's sidebar navigation stays consistent while only the content area changes
Relative Links in Nested Routes
When working with nested routes, you can use relative links to simplify navigation between related routes:
function Dashboard() {
return (
<div className="dashboard-container">
<div className="dashboard-sidebar">
<h2>Dashboard</h2>
<nav>
<ul>
{/* Relative links - relative to current route */}
<li><Link to=".">Overview</Link></li>
<li><Link to="profile">Profile</Link></li>
<li><Link to="settings">Settings</Link></li>
</ul>
</nav>
</div>
<div className="dashboard-content">
<Outlet />
</div>
</div>
);
}
Relative links work like this:
to="."- Links to the current routeto=".."- Links to the parent routeto="profile"- Links to a child route (relative to current)to="../other"- Links to a sibling route
This makes your routing more maintainable because you don't need to update all your links if you change the parent route path.
Route Paths in Nested Routes
When using nested routes, it's important to understand how paths combine:
| Parent Route | Child Route | Full Path |
|---|---|---|
| /dashboard | profile | /dashboard/profile |
| /users/:userId | posts | /users/:userId/posts |
| /app | settings/notifications | /app/settings/notifications |
Child routes can also have their own nested routes, creating deeply nested structures.
Index Routes
Index routes allow you to render a default child route when the parent route path is matched exactly. They're useful for providing a default view or home page within a nested route structure.
<Route path="/dashboard" element={<Dashboard />}>
{/* This renders at /dashboard */}
<Route index element={<DashboardOverview />} />
{/* These render at their respective paths */}
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
Without an index route, navigating to /dashboard would display the Dashboard layout but with an empty <Outlet />. The index route fills this gap by providing content for the parent route's exact path.
Nested Index Routes
You can use index routes at multiple levels of nesting:
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<DashboardOverview />} />
<Route path="settings" element={<Settings />}>
{/* This renders at /dashboard/settings */}
<Route index element={<GeneralSettings />} />
{/* These render at their respective paths */}
<Route path="account" element={<AccountSettings />} />
<Route path="notifications" element={<NotificationSettings />} />
</Route>
</Route>
In this example, navigating to /dashboard/settings would render the GeneralSettings component within the Settings component, which itself renders within the Dashboard component.
Real-World Example: E-commerce Categories
A practical example of index routes is in an e-commerce category structure:
<Route path="/shop" element={<Shop />}>
{/* Show featured products at /shop */}
<Route index element={<FeaturedProducts />} />
<Route path="clothing" element={<ClothingCategory />}>
{/* Show all clothing at /shop/clothing */}
<Route index element={<AllClothing />} />
{/* Subcategories */}
<Route path="mens" element={<MensClothing />} />
<Route path="womens" element={<WomensClothing />} />
<Route path="kids" element={<KidsClothing />} />
</Route>
<Route path="electronics" element={<ElectronicsCategory />}>
{/* Show all electronics at /shop/electronics */}
<Route index element={<AllElectronics />} />
{/* Subcategories */}
<Route path="phones" element={<Phones />} />
<Route path="computers" element={<Computers />} />
<Route path="accessories" element={<Accessories />} />
</Route>
</Route>
This structure creates a natural hierarchy that matches how customers browse an online store:
- Shop home shows featured products
- Category pages show all items in that category
- Subcategory pages show more specific items
Each level maintains consistent navigation and layout while changing only the products displayed.
Layout Patterns with Nested Routes
Nested routes enable powerful layout patterns that help structure your application UI in a consistent and maintainable way.
Basic Layout Pattern
The simplest layout pattern uses a parent route as a container with common elements, and child routes for the specific content:
function Layout() {
return (
<div className="app-container">
<header>
<h1>My App</h1>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</nav>
</header>
<main>
<Outlet />
</main>
<footer>
<p>© 2025 My App</p>
</footer>
</div>
);
}
// Using the layout
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Route>
</Routes>
Note that the parent route doesn't have a path specified, which means it matches all routes and acts purely as a layout container.
Multiple Layout Patterns
You can define different layouts for different sections of your application:
function MainLayout() {
return (
<div>
<MainHeader />
<Outlet />
<MainFooter />
</div>
);
}
function DashboardLayout() {
return (
<div className="dashboard-layout">
<DashboardHeader />
<div className="dashboard-container">
<DashboardSidebar />
<main>
<Outlet />
</main>
</div>
</div>
);
}
function AdminLayout() {
return (
<div className="admin-layout">
<AdminHeader />
<div className="admin-container">
<AdminSidebar />
<main>
<Outlet />
</main>
</div>
</div>
);
}
// Routes with different layouts
<Routes>
{/* Public pages with main layout */}
<Route element={<MainLayout />}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Route>
{/* User dashboard with dashboard layout */}
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
{/* Admin section with admin layout */}
<Route path="/admin" element={<AdminLayout />}>
<Route index element={<AdminHome />} />
<Route path="users" element={<UserManagement />} />
<Route path="settings" element={<AdminSettings />} />
</Route>
</Routes>
This approach creates a clear separation between different sections of your app, each with its own navigation and structure.
Real-World Example: Dashboard with Multiple Sections
Here's a more comprehensive example of a dashboard with multiple sections:
// DashboardLayout.js
function DashboardLayout() {
return (
<div className="dashboard">
<header className="dashboard-header">
<div className="logo">MyApp</div>
<div className="search-bar">
<input type="text" placeholder="Search..." />
</div>
<div className="user-menu">
<UserMenu />
</div>
</header>
<div className="dashboard-container">
<nav className="dashboard-sidebar">
<ul>
<li>
<NavLink to="/dashboard" end>
<span className="icon">📊</span> Overview
</NavLink>
</li>
<li>
<NavLink to="/dashboard/analytics">
<span className="icon">📈</span> Analytics
</NavLink>
</li>
<li>
<NavLink to="/dashboard/projects">
<span className="icon">📁</span> Projects
</NavLink>
</li>
<li>
<NavLink to="/dashboard/messages">
<span className="icon">✉️</span> Messages
</NavLink>
</li>
<li>
<NavLink to="/dashboard/settings">
<span className="icon">⚙️</span> Settings
</NavLink>
</li>
</ul>
</nav>
<main className="dashboard-content">
<Outlet />
</main>
</div>
</div>
);
}
// Dashboard routing
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardOverview />} />
<Route path="analytics" element={<Analytics />} />
{/* Projects section with its own nested routes */}
<Route path="projects" element={<Projects />}>
<Route index element={<ProjectsList />} />
<Route path=":projectId" element={<ProjectDetails />} />
<Route path="new" element={<NewProject />} />
</Route>
{/* Messages section with its own nested routes */}
<Route path="messages" element={<Messages />}>
<Route index element={<InboxList />} />
<Route path=":messageId" element={<MessageDetail />} />
<Route path="compose" element={<ComposeMessage />} />
</Route>
{/* Settings section with its own nested routes */}
<Route path="settings" element={<Settings />}>
<Route index element={<GeneralSettings />} />
<Route path="profile" element={<ProfileSettings />} />
<Route path="notifications" element={<NotificationSettings />} />
<Route path="security" element={<SecuritySettings />} />
</Route>
</Route>
This structured approach creates a consistent dashboard experience while allowing for deep navigation within each section.
Nested Layout Components
You can create even more specific layouts by nesting layout components. This is particularly useful for sections that have their own navigation or structure.
// Projects section with its own layout
function ProjectsLayout() {
return (
<div className="projects-container">
<div className="projects-header">
<h1>Projects</h1>
<Link to="/dashboard/projects/new" className="button">
New Project
</Link>
</div>
<div className="projects-tabs">
<NavLink to="/dashboard/projects" end>All Projects</NavLink>
<NavLink to="/dashboard/projects/active">Active</NavLink>
<NavLink to="/dashboard/projects/archived">Archived</NavLink>
</div>
<div className="projects-content">
<Outlet />
</div>
</div>
);
}
// Using the nested layout
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardOverview />} />
{/* Projects section with its own layout */}
<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>
{/* Other dashboard routes */}
</Route>
This creates a three-level hierarchy:
- DashboardLayout (header, sidebar, content area)
- ProjectsLayout (projects header, tabs, content area)
- Specific project views (all, active, archived, details, etc.)
Split Pane Layouts
Another common layout pattern is the split pane or master-detail view, often used for email clients, chat applications, or document management systems:
function MessagesLayout() {
const { messageId } = useParams();
return (
<div className="messages-container">
<div className="messages-sidebar">
<div className="messages-actions">
<Link to="/dashboard/messages/compose" className="button">
Compose
</Link>
</div>
<div className="messages-folders">
<NavLink to="/dashboard/messages" end>Inbox</NavLink>
<NavLink to="/dashboard/messages/sent">Sent</NavLink>
<NavLink to="/dashboard/messages/drafts">Drafts</NavLink>
<NavLink to="/dashboard/messages/trash">Trash</NavLink>
</div>
<MessageList selectedId={messageId} />
</div>
<div className="messages-content">
<Outlet />
</div>
</div>
);
}
// Using the split pane layout
<Route path="/dashboard/messages" element={<MessagesLayout />}>
<Route index element={<NoMessageSelected />} />
<Route path=":messageId" element={<MessageDetail />} />
<Route path="compose" element={<ComposeMessage />} />
<Route path="sent" element={<SentMessages />} />
<Route path="drafts" element={<DraftMessages />} />
<Route path="trash" element={<TrashMessages />} />
</Route>
Real-World Example: Multi-Panel Admin Interface
Administrative interfaces often have complex layout requirements. Here's how you might structure one:
// AdminLayout.js
function AdminLayout() {
return (
<div className="admin-layout">
<header className="admin-header">
<div className="logo">Admin Panel</div>
<div className="admin-search">
<input type="text" placeholder="Search..." />
</div>
<div className="admin-user-menu">
<AdminUserMenu />
</div>
</header>
<div className="admin-container">
<nav className="admin-sidebar">
<AdminSidebarMenu />
</nav>
<main className="admin-content">
<Outlet />
</main>
</div>
</div>
);
}
// UsersManagement.js
function UsersManagement() {
const [searchParams, setSearchParams] = useSearchParams();
const page = parseInt(searchParams.get('page') || '1', 10);
const perPage = parseInt(searchParams.get('perPage') || '10', 10);
const sortBy = searchParams.get('sortBy') || 'name';
const sortOrder = searchParams.get('sortOrder') || 'asc';
return (
<div className="users-management">
<header className="content-header">
<h1>Users Management</h1>
<div className="actions">
<Link to="/admin/users/new" className="button">
Add User
</Link>
<button className="button">Export</button>
</div>
</header>
<div className="filters">
<div className="filter-group">
<label>Role:</label>
<select
value={searchParams.get('role') || ''}
onChange={e => {
const newParams = new URLSearchParams(searchParams);
if (e.target.value) {
newParams.set('role', e.target.value);
} else {
newParams.delete('role');
}
setSearchParams(newParams);
}}
>
<option value="">All Roles</option>
<option value="admin">Admin</option>
<option value="editor">Editor</option>
<option value="user">User</option>
</select>
</div>
<div className="filter-group">
{/* More filters */}
</div>
</div>
<div className="content-body">
<div className="data-table">
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{/* Table rows here */}
</tbody>
</table>
</div>
<div className="pagination">
<button
disabled={page === 1}
onClick={() => {
const newParams = new URLSearchParams(searchParams);
newParams.set('page', (page - 1).toString());
setSearchParams(newParams);
}}
>
Previous
</button>
<span>Page {page}</span>
<button
onClick={() => {
const newParams = new URLSearchParams(searchParams);
newParams.set('page', (page + 1).toString());
setSearchParams(newParams);
}}
>
Next
</button>
</div>
</div>
</div>
);
}
// Admin routes configuration
<Route path="/admin" element={<AdminLayout />}>
<Route index element={<AdminDashboard />} />
{/* Users management */}
<Route path="users" element={<Outlet />}>
<Route index element={<UsersManagement />} />
<Route path=":userId" element={<UserDetails />} />
<Route path="new" element={<NewUser />} />
</Route>
{/* Products management */}
<Route path="products" element={<Outlet />}>
<Route index element={<ProductsManagement />} />
<Route path=":productId" element={<ProductDetails />} />
<Route path="new" element={<NewProduct />} />
<Route path="categories" element={<ProductCategories />} />
</Route>
{/* Settings */}
<Route path="settings" element={<AdminSettings />} />
</Route>
This example demonstrates multiple levels of UI hierarchy:
- The admin layout with global header and sidebar
- Content sections with their own headers, filters, and actions
- Data tables with sorting, filtering, and pagination
- Detail views for individual records
All of this is organized using nested routes, with each level adding its own UI structure.
Sharing Data Between Routes
Nested routes provide opportunities to share data and state between parent and child routes. This can be achieved in several ways:
Using Context
Create a context in the parent route and provide it to all child routes:
// Create a context for the dashboard
const DashboardContext = createContext();
function DashboardLayout() {
const [sidebarOpen, setSidebarOpen] = useState(true);
const [theme, setTheme] = useState('light');
// Data and functions to share with child routes
const dashboardValue = {
sidebarOpen,
toggleSidebar: () => setSidebarOpen(prev => !prev),
theme,
toggleTheme: () => setTheme(prev => prev === 'light' ? 'dark' : 'light')
};
return (
<DashboardContext.Provider value={dashboardValue}>
<div className={`dashboard ${theme}`}>
<header className="dashboard-header">
{/* Header content */}
<button onClick={dashboardValue.toggleSidebar}>
{sidebarOpen ? 'Hide' : 'Show'} Sidebar
</button>
<button onClick={dashboardValue.toggleTheme}>
Toggle Theme
</button>
</header>
<div className="dashboard-container">
{sidebarOpen && (
<nav className="dashboard-sidebar">
{/* Sidebar content */}
</nav>
)}
<main className="dashboard-content">
<Outlet />
</main>
</div>
</div>
</DashboardContext.Provider>
);
}
// Hook to use dashboard context in child routes
export function useDashboard() {
const context = useContext(DashboardContext);
if (!context) {
throw new Error('useDashboard must be used within a DashboardLayout');
}
return context;
}
// Using the shared context in a child route
function DashboardOverview() {
const { theme, toggleTheme } = useDashboard();
return (
<div>
<h1>Dashboard Overview</h1>
<p>Current theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}
Using useOutletContext
React Router provides useOutletContext as a simpler alternative to creating your own context:
import { Outlet, useOutletContext } from 'react-router-dom';
function DashboardLayout() {
const [sidebarOpen, setSidebarOpen] = useState(true);
const [theme, setTheme] = useState('light');
// Data and functions to share with child routes
const dashboardContext = {
sidebarOpen,
toggleSidebar: () => setSidebarOpen(prev => !prev),
theme,
toggleTheme: () => setTheme(prev => prev === 'light' ? 'dark' : 'light')
};
return (
<div className={`dashboard ${theme}`}>
{/* Dashboard layout */}
{/* Pass context to Outlet */}
<main className="dashboard-content">
<Outlet context={dashboardContext} />
</main>
</div>
);
}
// In a child route
function DashboardOverview() {
// Access the context provided by the parent route
const { theme, toggleTheme } = useOutletContext();
return (
<div>
<h1>Dashboard Overview</h1>
<p>Current theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}
Using URL Parameters
Another way to share data between routes is through URL parameters:
// Routes with parameters
<Route path="/projects" element={<ProjectsLayout />}>
<Route path=":projectId" element={<ProjectDetails />}>
<Route index element={<ProjectOverview />} />
<Route path="tasks" element={<ProjectTasks />} />
<Route path="team" element={<ProjectTeam />} />
<Route path="settings" element={<ProjectSettings />} />
</Route>
</Route>
// Parent route accessing the parameter
function ProjectDetails() {
const { projectId } = useParams();
const [project, setProject] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch project data
const fetchProject = async () => {
setLoading(true);
try {
const response = await fetch(`/api/projects/${projectId}`);
const data = await response.json();
setProject(data);
} catch (error) {
console.error('Error fetching project:', error);
} finally {
setLoading(false);
}
};
fetchProject();
}, [projectId]);
if (loading) {
return <div>Loading project...</div>;
}
if (!project) {
return <div>Project not found</div>;
}
// Pass project data to child routes via context
return (
<div className="project-details">
<header>
<h1>{project.name}</h1>
<div className="project-meta">
<span>Status: {project.status}</span>
<span>Due: {new Date(project.dueDate).toLocaleDateString()}</span>
</div>
</header>
<nav className="project-tabs">
<NavLink to={``} end>Overview</NavLink>
<NavLink to={`tasks`}>Tasks</NavLink>
<NavLink to={`team`}>Team</NavLink>
<NavLink to={`settings`}>Settings</NavLink>
</nav>
<div className="project-content">
<Outlet context={project} />
</div>
</div>
);
}
// Child route using the project data
function ProjectTasks() {
const project = useOutletContext();
return (
<div>
<h2>Tasks for {project.name}</h2>
<ul>
{project.tasks.map(task => (
<li key={task.id}>{task.title}</li>
))}
</ul>
</div>
);
}
Data Flow in Nested Routes
Here's how data typically flows in a nested route structure:
This pattern is particularly powerful for:
- Complex data-driven interfaces
- Master-detail views
- Wizards and multi-step forms
- Tabbed interfaces for viewing different aspects of a resource
Practice Activities
Activity 1: Basic Nested Routes
Create a simple application with the following nested route structure:
- A main layout with header, sidebar, and content area
- Home, About, and Contact routes that render within the layout
- A dashboard route with its own nested routes:
- Dashboard overview (index route)
- Profile page
- Settings page
Ensure that navigation works correctly at all levels.
Activity 2: Complex Layout Patterns
Extend the application to include more advanced layout patterns:
- Create multiple layouts for different sections (main, dashboard, admin)
- Implement a settings section with its own nested routes and tabs
- Create a master-detail view for a list of products
- Add an admin section with tables, filtering, and pagination
Focus on creating reusable layout components and consistent navigation.
Activity 3: Data Sharing and Context
Implement data sharing between nested routes:
- Create a context for the dashboard that provides theme settings and UI state
- Use
useOutletContextto pass data from a parent route to its children - Implement a project details page that fetches data and shares it with child routes
- Create a shopping cart that persists across different product pages
Ensure that data flows correctly between related routes and that UI state is consistent.
Summary
- Nested routes allow you to create hierarchical routing structures that match your UI organization
- The
Outletcomponent acts as a placeholder where child routes are rendered - Index routes provide default content for parent routes when they match exactly
- Layout patterns using nested routes create consistent UI sections with shared elements
- Multiple levels of nesting enable complex UI hierarchies for dashboards and admin interfaces
- Data can be shared between parent and child routes using context,
useOutletContext, or URL parameters - Relative links simplify navigation between related routes