AdSense Leaderboard

Google AdSense Placeholder

HEADER SLOT

Nested Routes in React Router: Complete Guide to Layout Patterns

Last updated:
Nested Routes in React Router: Complete Guide to Layout Patterns

Master nested routes in React Router with practical examples. Learn layout patterns, Outlet component, dynamic segments, and production-ready patterns for building scalable SPAs.

# 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

  1. Understanding Nested Routes
  2. The Outlet Component: Where Child Routes Render
  3. Basic Nested Route Structure
  4. Layout Routes vs Child Routes
  5. Practical Example: Multi-Level Dashboard
  6. Dynamic Routes with Nesting
  7. Common Pitfalls and Solutions
  8. Advanced Patterns
  9. 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:

typescript
// ❌ 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:

typescript
// ✅ 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

typescript
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>
    </>
  );
}
javascript
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:

typescript
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>;
}
javascript
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

typescript
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} />;
}
javascript
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

typescript
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>
  );
}
javascript
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> with className function 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

typescript
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} />;
}
javascript
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:

typescript
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>
  );
}
javascript
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:

typescript
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 />
    </>
  );
}
javascript
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 → Shows CustomersLayout with CustomersList
  • /customers/123 → Shows CustomersList in sidebar, CustomerDetail with CustomerProfile in main area
  • /customers/123/history → Shows CustomersList in sidebar, CustomerDetail with CustomerHistory in 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

typescript
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>
  );
}
javascript
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.

typescript
// ❌ 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.

typescript
// ❌ 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.

typescript
// ❌ 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:

typescript
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>
        ),
      },
    ],
  },
]);
javascript
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:

typescript
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>
  );
}
javascript
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:

typescript
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 }} />
    </>
  );
}
javascript
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:

  1. Every layout route must have an <Outlet /> or child routes won't render
  2. Use index: true for default child routes
  3. Leverage NavLink with dynamic className for active link styling
  4. Use useParams() to access parent route parameters in child components
  5. 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?

Sponsored Content

Google AdSense Placeholder

CONTENT SLOT

Sponsored

Google AdSense Placeholder

FOOTER SLOT