Skip to main content

Nested Routes and Layouts: Build Shared Dashboards with React Router

Nested routes and the <Outlet> component enable you to build complex, multi-page layouts where a parent route defines the shared layout (header, sidebar, navigation) and child routes define content that changes. This pattern is essential for building dashboards, admin panels, and any application where multiple pages share a common structure without duplication.

Key Takeaways

  • Nested routes mirror your UI hierarchy: parent routes define layouts, child routes define page content
  • The <Outlet> component is a placeholder in the parent layout where child route content is rendered
  • Child route paths are relative to the parent's path: child profile under parent /dashboard creates /dashboard/profile
  • The index prop renders a child route when the parent path matches exactly (without a child path)
  • Nested layouts eliminate code duplication and keep navigation logic centralized
  • You can nest routes multiple levels deep for complex applications like admin panels

Prerequisites

Before reading this article, you should understand:

  • React Router fundamentals: <Routes>, <Route>, and <Link>
  • How to create and import React components
  • Basic routing concepts from earlier articles in this series

Why Nested Routes Matter

In a typical dashboard application, you have a shared structure:

  • A navigation sidebar with links to Dashboard, Profile, and Settings
  • A header or branding area that never changes
  • A main content area that switches between pages

Without nested routes, you would duplicate the navigation and header in every page component. Nested routes allow you to define the shared layout once and render different content within it.

Building a Dashboard with Nested Routes

Here's a complete example of a dashboard with a shared layout and three child pages.

Step 1: Create the Layout Component

The DashboardLayout component contains the shared navigation and the <Outlet> placeholder:

import React from 'react';
import { Link, Outlet } from 'react-router-dom';

function DashboardLayout() {
return (
<div style={{ display: 'flex' }}>
{/* Sidebar Navigation */}
<nav style={{ width: '200px', borderRight: '1px solid #ccc', padding: '20px' }}>
<h3>Dashboard</h3>
<ul style={{ listStyle: 'none', padding: 0 }}>
<li>
<Link to="/dashboard">Home</Link>
</li>
<li>
<Link to="/dashboard/profile">Profile</Link>
</li>
<li>
<Link to="/dashboard/settings">Settings</Link>
</li>
</ul>
</nav>

{/* Main Content Area */}
<main style={{ flex: 1, padding: '20px' }}>
<Outlet />
</main>
</div>
);
}

export default DashboardLayout;

The <Outlet> component acts as a render point for child route components. When the user navigates to /dashboard/profile, the Profile component replaces the <Outlet>.

Step 2: Create Child Page Components

Create simple components for each dashboard page:

// DashboardHome.js
function DashboardHome() {
return (
<div>
<h2>Dashboard Home</h2>
<p>Welcome to your dashboard.</p>
</div>
);
}

export default DashboardHome;
// Profile.js
function Profile() {
return (
<div>
<h2>User Profile</h2>
<p>Manage your profile information here.</p>
</div>
);
}

export default Profile;
// Settings.js
function Settings() {
return (
<div>
<h2>Settings</h2>
<p>Adjust your account settings here.</p>
</div>
);
}

export default Settings;

Step 3: Define Nested Routes in Your App

In your main App.js, define the nested route structure:

import React from 'react';
import { Routes, Route } from 'react-router-dom';
import DashboardLayout from './DashboardLayout';
import DashboardHome from './DashboardHome';
import Profile from './Profile';
import Settings from './Settings';

function App() {
return (
<Routes>
{/* Parent route defines the layout */}
<Route path="/dashboard" element={<DashboardLayout />}>
{/* Child routes inherit the parent's path */}
<Route index element={<DashboardHome />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>

{/* Other routes outside the dashboard */}
<Route path="/" element={<Home />} />
</Routes>
);
}

export default App;

How this routing structure works:

  • /dashboard — Parent route renders DashboardLayout; the index route renders DashboardHome inside the <Outlet>
  • /dashboard/profile — Parent route renders DashboardLayout; child route renders Profile inside the <Outlet>
  • /dashboard/settings — Parent route renders DashboardLayout; child route renders Settings inside the <Outlet>

Understanding the index Prop

The index prop indicates that a child route matches when its parent route path matches exactly, without requiring additional path segments:

<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} /> {/* Matches /dashboard */}
<Route path="profile" element={<Profile />} /> {/* Matches /dashboard/profile */}
</Route>

Without index, navigating to /dashboard alone would render the DashboardLayout but leave the <Outlet> empty.

Multi-Level Nesting: Complex Layouts

You can nest routes multiple levels deep. For example, a Settings page might have sub-pages:

<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="profile" element={<Profile />} />

{/* Nested layout for settings */}
<Route path="settings" element={<SettingsLayout />}>
<Route index element={<GeneralSettings />} />
<Route path="account" element={<AccountSettings />} />
<Route path="privacy" element={<PrivacySettings />} />
</Route>
</Route>

Now /dashboard/settings renders SettingsLayout with GeneralSettings in its <Outlet>, and /dashboard/settings/account renders AccountSettings in the same <Outlet>.

Use <Link> to navigate within nested routes:

<Link to="/dashboard/profile">Go to Profile</Link>
<Link to="/dashboard/settings">Go to Settings</Link>

Or use the useNavigate hook for programmatic navigation:

import { useNavigate } from 'react-router-dom';

function MyComponent() {
const navigate = useNavigate();

const handleClick = () => {
navigate('/dashboard/profile');
};

return <button onClick={handleClick}>View Profile</button>;
}

Passing Data to Nested Routes

Nested routes can access URL parameters just like regular routes:

<Route path="/dashboard" element={<DashboardLayout />}>
<Route path="user/:userId" element={<UserDetail />} />
</Route>

// In UserDetail.js
import { useParams } from 'react-router-dom';

function UserDetail() {
const { userId } = useParams();
return <h2>User {userId}</h2>;
}

URLs like /dashboard/user/123 will render the DashboardLayout and display the user with userId of 123 in the <Outlet>.

Complete Working Example

Here's the entire dashboard application in one snippet:

// App.js
import React from 'react';
import { Routes, Route, Link, Outlet } from 'react-router-dom';

function DashboardLayout() {
return (
<div style={{ display: 'flex' }}>
<nav style={{ width: '200px', padding: '20px', borderRight: '1px solid #ddd' }}>
<h3>Navigation</h3>
<ul>
<li><Link to="/dashboard">Home</Link></li>
<li><Link to="/dashboard/profile">Profile</Link></li>
<li><Link to="/dashboard/settings">Settings</Link></li>
</ul>
</nav>
<main style={{ flex: 1, padding: '20px' }}>
<Outlet />
</main>
</div>
);
}

function DashboardHome() {
return <h2>Welcome to Dashboard</h2>;
}

function Profile() {
return <h2>Your Profile</h2>;
}

function Settings() {
return <h2>Settings</h2>;
}

function App() {
return (
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
);
}

export default App;

Best Practices

1. Keep Layouts Focused on Structure

Layouts should contain navigation and UI framework, not business logic. Move complex state and side effects to page components.

2. Use Relative Paths in Child Routes

Child paths are relative to the parent, so avoid repeating the parent path:

// Good
<Route path="/dashboard" element={<DashboardLayout />}>
<Route path="profile" element={<Profile />} />
</Route>

// Avoid
<Route path="/dashboard" element={<DashboardLayout />}>
<Route path="/dashboard/profile" element={<Profile />} />
</Route>

3. Organize Route Definitions

For large applications, extract nested routes into separate constants:

const dashboardRoutes = [
{ path: '/', element: <DashboardHome /> },
{ path: '/profile', element: <Profile /> },
{ path: '/settings', element: <Settings /> }
];

<Route path="/dashboard" element={<DashboardLayout />}>
{dashboardRoutes.map(route => (
<Route key={route.path} path={route.path} element={route.element} />
))}
</Route>

Frequently Asked Questions

What's the difference between nested routes and a layout wrapper?

Nested routes use React Router's <Outlet> and require formal route definitions. A layout wrapper is a higher-order component that wraps route components. Nested routes are cleaner for complex hierarchies because they mirror your routing structure.

Can I have multiple outlets in a layout?

React Router supports named outlets via the outlet prop (v6.4+), but single <Outlet> is most common. For multiple independent content areas, consider splitting them into separate routes or using state/context.

How do I access the parent route's data in a child component?

Use URL parameters (e.g., /dashboard/:dashboardId/profile) or React Context. The child component can access them with useParams() or context hooks.

What happens if no child route matches the current URL?

The <Outlet> renders nothing. To handle this, create a catch-all route with path="*" that shows a "Not Found" message:

<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="*" element={<NotFound />} />
</Route>

Can I lazy-load nested route components?

Yes, use React.lazy() and <Suspense>:

const Profile = React.lazy(() => import('./Profile'));

<Route path="profile" element={<Suspense fallback={<div>Loading...</div>}><Profile /></Suspense>} />

Further Reading