Nested Routes

Difficulty: Intermediate

Nested routes allow you to define routes inside other routes, creating a hierarchical structure where parent routes render shared layout (like navigation bars, sidebars, or headers) and child routes render their specific content within that layout. This mirrors the natural structure of most web applications, where pages share common UI elements.

In React Router v6, you nest Route elements inside parent Route elements. The parent route's component renders an Outlet component that acts as a placeholder for where the matched child route's component will appear. When the URL matches a child route, React Router renders both the parent layout and the child content inside the Outlet.

For example, a dashboard layout with a sidebar might have routes like: /dashboard (overview), /dashboard/analytics, and /dashboard/settings. The parent route at /dashboard renders the sidebar and an Outlet. Each child route renders its specific content inside that Outlet. The sidebar stays on screen across all dashboard pages without re-mounting.

Layout routes are a special case of nested routes where the parent route has no path. They exist purely to wrap child routes in shared UI. For instance, you might have a layout route that provides a header and footer around all pages, without adding any segment to the URL. The children inherit the layout but define their own paths.

Index routes (using the index prop instead of path) serve as the default child when the parent route's URL is matched exactly. For /dashboard, the index route renders the overview component. When the user navigates to /dashboard/analytics, the index route is replaced by the analytics component. This ensures there's always content in the Outlet, even at the parent URL.

Code examples

Nested route structure

// Simulating nested route rendering
function DashboardLayout(activeChild) {
  console.log('--- Dashboard Layout ---');
  console.log('Sidebar: [Overview] [Analytics] [Settings]');
  console.log('Content area (Outlet):');
  console.log('  ' + activeChild);
  console.log('--- End Layout ---');
}

// Simulating route matching
function renderRoute(path) {
  console.log('URL: ' + path);

  if (path === '/dashboard') {
    DashboardLayout('DashboardOverview (index route)');
  } else if (path === '/dashboard/analytics') {
    DashboardLayout('AnalyticsPage');
  } else if (path === '/dashboard/settings') {
    DashboardLayout('SettingsPage');
  }
  console.log('');
}

renderRoute('/dashboard');
renderRoute('/dashboard/analytics');
renderRoute('/dashboard/settings');

The Dashboard Layout (sidebar) persists across all child routes. Only the content inside the Outlet changes. This is the power of nested routes - shared UI stays mounted while child content swaps.

Outlet as a placeholder

function Outlet(childContent) {
  console.log('Outlet renders: ' + childContent);
}

function AppLayout(child) {
  console.log('Header: My App');
  Outlet(child);
  console.log('Footer: Copyright 2024');
}

// Different pages rendered through the layout
console.log('--- Home Page ---');
AppLayout('Welcome to the home page');
console.log('');
console.log('--- About Page ---');
AppLayout('About our company');
console.log('');
console.log('--- Contact Page ---');
AppLayout('Get in touch with us');

Outlet is where child route content appears inside the parent layout. The Header and Footer are rendered by the parent and persist across all child routes. Only the Outlet content changes.

Index routes

function resolveRoute(path, routes) {
  // Check for exact match first
  const exact = routes.find(r => r.path === path && !r.index);
  if (exact) {
    console.log('Matched: ' + exact.path + ' -> ' + exact.component);
    return;
  }

  // Check for index route (default child)
  const parent = routes.find(r => path.startsWith(r.path) && r.children);
  if (parent) {
    const indexRoute = parent.children.find(r => r.index);
    const childRoute = parent.children.find(r => r.path && path === parent.path + '/' + r.path);

    if (childRoute) {
      console.log('Parent: ' + parent.component);
      console.log('Child: ' + childRoute.component);
    } else if (indexRoute) {
      console.log('Parent: ' + parent.component);
      console.log('Index (default): ' + indexRoute.component);
    }
  }
}

const routes = [
  {
    path: '/dashboard',
    component: 'DashboardLayout',
    children: [
      { index: true, component: 'Overview' },
      { path: 'stats', component: 'Stats' },
      { path: 'reports', component: 'Reports' }
    ]
  }
];

console.log('URL: /dashboard');
resolveRoute('/dashboard', routes);
console.log('');
console.log('URL: /dashboard/stats');
resolveRoute('/dashboard/stats', routes);

The index route renders at the parent URL (/dashboard) as the default content. When a child path matches (/dashboard/stats), that child replaces the index route in the Outlet.

Key points

Concepts covered

nested routes, Outlet, layout routes, parent-child routing, route hierarchy