Nested Routes: Building Complex UIs

Creating Hierarchical Navigation Structures

Welcome to Nested Routes!

Imagine building a house where each room can contain other rooms, and those rooms can contain even more rooms. That's nested routing! It allows us to create complex, hierarchical UIs where components can have their own child routes, creating a natural and intuitive navigation structure.

graph TD A[App Layout] --> B[Dashboard] B --> C[Overview] B --> D[Analytics] B --> E[Reports] E --> F[Sales Report] E --> G[User Report] E --> H[Product Report] A --> I[Settings] I --> J[Profile] I --> K[Security] I --> L[Notifications] style A fill:#ff9999 style B fill:#99ff99 style E fill:#99ff99 style I fill:#99ff99

Nested Routes Fundamentals

1. Basic Nested Structure

import { Routes, Route, Outlet } from 'react-router-dom';

// Parent layout component
function DashboardLayout() {
  return (
    <div className="dashboard">
      <nav className="dashboard-nav">
        <Link to="/dashboard">Overview</Link>
        <Link to="/dashboard/analytics">Analytics</Link>
        <Link to="/dashboard/reports">Reports</Link>
      </nav>
      
      <div className="dashboard-content">
        {/* Child routes render here */}
        <Outlet />
      </div>
    </div>
  );
}

// Route configuration
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      
      {/* Nested routes */}
      <Route path="/dashboard" element={<DashboardLayout />}>
        <Route index element={<DashboardOverview />} />
        <Route path="analytics" element={<Analytics />} />
        <Route path="reports" element={<Reports />} />
      </Route>
      
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

2. Multiple Levels of Nesting

// Deep nesting example
function App() {
  return (
    <Routes>
      <Route path="/" element={<MainLayout />}>
        <Route index element={<Home />} />
        
        <Route path="dashboard" element={<DashboardLayout />}>
          <Route index element={<DashboardHome />} />
          
          <Route path="reports" element={<ReportsLayout />}>
            <Route index element={<ReportsList />} />
            <Route path="sales" element={<SalesReport />} />
            <Route path="users" element={<UsersReport />} />
            
            <Route path="custom" element={<CustomReportsLayout />}>
              <Route index element={<CustomReportBuilder />} />
              <Route path=":reportId" element={<CustomReport />} />
              <Route path=":reportId/edit" element={<EditCustomReport />} />
            </Route>
          </Route>
          
          <Route path="settings" element={<SettingsLayout />}>
            <Route index element={<GeneralSettings />} />
            <Route path="profile" element={<ProfileSettings />} />
            <Route path="security" element={<SecuritySettings />} />
          </Route>
        </Route>
        
        <Route path="admin" element={<AdminLayout />}>
          {/* Admin routes */}
        </Route>
      </Route>
    </Routes>
  );
}

// Layout components with nested Outlets
function MainLayout() {
  return (
    <div className="main-layout">
      <Header />
      <main>
        <Outlet /> {/* Renders dashboard, admin, etc. */}
      </main>
      <Footer />
    </div>
  );
}

function DashboardLayout() {
  return (
    <div className="dashboard-layout">
      <DashboardSidebar />
      <div className="dashboard-main">
        <Outlet /> {/* Renders reports, settings, etc. */}
      </div>
    </div>
  );
}

function ReportsLayout() {
  return (
    <div className="reports-layout">
      <ReportsNav />
      <div className="reports-content">
        <Outlet /> {/* Renders specific reports */}
      </div>
    </div>
  );
}

Outlet Context

1. Passing Data Through Outlets

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

// Parent component providing context
function UserDashboard() {
  const [user, setUser] = useState(null);
  const [preferences, setPreferences] = useState({});
  
  useEffect(() => {
    fetchUserData().then(data => {
      setUser(data.user);
      setPreferences(data.preferences);
    });
  }, []);
  
  const updatePreferences = (newPrefs) => {
    setPreferences(prev => ({ ...prev, ...newPrefs }));
    savePreferences(newPrefs);
  };
  
  return (
    <div className="user-dashboard">
      <UserHeader user={user} />
      <div className="dashboard-content">
        <Outlet context={{ user, preferences, updatePreferences }} />
      </div>
    </div>
  );
}

// Child component consuming context
function UserProfile() {
  const { user, preferences, updatePreferences } = useOutletContext();
  
  if (!user) return <Loading />;
  
  return (
    <div className="user-profile">
      <h2>{user.name}'s Profile</h2>
      <PreferencesForm 
        preferences={preferences}
        onUpdate={updatePreferences}
      />
    </div>
  );
}

// Another child component using the same context
function UserSettings() {
  const { user, preferences } = useOutletContext();
  
  return (
    <div className="user-settings">
      <h2>Settings for {user?.name}</h2>
      <ThemeSelector 
        currentTheme={preferences.theme}
        onChange={(theme) => updatePreferences({ theme })}
      />
    </div>
  );
}

2. Complex Context Patterns

// Multi-level context passing
function AppLayout() {
  const [globalState, setGlobalState] = useState({
    theme: 'light',
    notifications: [],
    user: null
  });
  
  return (
    <div className={`app theme-${globalState.theme}`}>
      <Outlet context={{ globalState, setGlobalState }} />
    </div>
  );
}

function DashboardLayout() {
  const { globalState, setGlobalState } = useOutletContext();
  const [dashboardData, setDashboardData] = useState(null);
  
  // Combine parent context with local data
  const combinedContext = {
    ...globalState,
    dashboardData,
    setDashboardData,
    setGlobalState
  };
  
  return (
    <div className="dashboard">
      <DashboardNav />
      <Outlet context={combinedContext} />
    </div>
  );
}

function ReportsSection() {
  const context = useOutletContext();
  const [reportFilters, setReportFilters] = useState({});
  
  // Further extend context
  const extendedContext = {
    ...context,
    reportFilters,
    setReportFilters
  };
  
  return (
    <div className="reports">
      <FilterBar filters={reportFilters} onChange={setReportFilters} />
      <Outlet context={extendedContext} />
    </div>
  );
}

Dynamic Nested Routes

1. Dynamic Route Generation

// Dynamic nested routes based on data
function DynamicRoutes() {
  const [categories, setCategories] = useState([]);
  
  useEffect(() => {
    fetchCategories().then(setCategories);
  }, []);
  
  return (
    <Routes>
      <Route path="/" element={<MainLayout />}>
        <Route index element={<Home />} />
        
        <Route path="categories" element={<CategoriesLayout />}>
          <Route index element={<CategoriesList />} />
          
          {/* Dynamically generated routes */}
          {categories.map(category => (
            <Route 
              key={category.id}
              path={category.slug}
              element={<CategoryPage category={category} />}
            >
              <Route index element={<CategoryOverview />} />
              <Route path="products" element={<CategoryProducts />} />
              <Route path="subcategories" element={<SubcategoriesList />} />
              
              {/* Nested dynamic routes */}
              {category.subcategories?.map(sub => (
                <Route
                  key={sub.id}
                  path={`subcategories/${sub.slug}`}
                  element={<SubcategoryPage subcategory={sub} />}
                />
              ))}
            </Route>
          ))}
        </Route>
      </Route>
    </Routes>
  );
}

// Component-based route configuration
function CategoryRoutes({ category }) {
  const subcategoryRoutes = category.subcategories.map(sub => ({
    path: sub.slug,
    element: <SubcategoryPage subcategory={sub} />,
    children: [
      {
        index: true,
        element: <SubcategoryOverview />
      },
      {
        path: 'products',
        element: <SubcategoryProducts />
      }
    ]
  }));
  
  return (
    <Routes>
      <Route index element={<CategoryOverview />} />
      <Route path="products" element={<CategoryProducts />} />
      <Route path="subcategories">
        <Route index element={<SubcategoriesList />} />
        {subcategoryRoutes.map(route => (
          <Route key={route.path} {...route} />
        ))}
      </Route>
    </Routes>
  );
}

2. Conditional Nested Routes

// Routes based on user permissions
function ConditionalRoutes() {
  const { user } = useAuth();
  
  return (
    <Routes>
      <Route path="/" element={<AppLayout />}>
        <Route index element={<Home />} />
        
        <Route path="account" element={<AccountLayout />}>
          <Route index element={<AccountOverview />} />
          <Route path="profile" element={<Profile />} />
          
          {/* Conditional routes based on user role */}
          {user?.role === 'admin' && (
            <>
              <Route path="admin" element={<AdminSection />}>
                <Route index element={<AdminDashboard />} />
                <Route path="users" element={<UserManagement />} />
                <Route path="settings" element={<AdminSettings />} />
              </Route>
            </>
          )}
          
          {user?.role === 'editor' && (
            <Route path="content" element={<ContentSection />}>
              <Route index element={<ContentList />} />
              <Route path="create" element={<CreateContent />} />
              <Route path="edit/:id" element={<EditContent />} />
            </Route>
          )}
          
          {user?.hasSubscription && (
            <Route path="premium" element={<PremiumSection />}>
              <Route index element={<PremiumContent />} />
              <Route path="exclusive" element={<ExclusiveContent />} />
            </Route>
          )}
        </Route>
      </Route>
    </Routes>
  );
}

Layout Composition Patterns

1. Shared Layouts

// Reusable layout components
function SidebarLayout({ title, navigation }) {
  return (
    <div className="sidebar-layout">
      <aside className="sidebar">
        <h2>{title}</h2>
        <nav>
          {navigation.map(item => (
            <NavLink 
              key={item.path} 
              to={item.path}
              className={({ isActive }) => isActive ? 'active' : ''}
            >
              {item.icon && <item.icon />}
              {item.label}
            </NavLink>
          ))}
        </nav>
      </aside>
      <main className="main-content">
        <Outlet />
      </main>
    </div>
  );
}

// Using shared layouts
function App() {
  const dashboardNav = [
    { path: '/dashboard', label: 'Overview', icon: HomeIcon },
    { path: '/dashboard/analytics', label: 'Analytics', icon: ChartIcon },
    { path: '/dashboard/reports', label: 'Reports', icon: DocumentIcon }
  ];
  
  const settingsNav = [
    { path: '/settings', label: 'General', icon: CogIcon },
    { path: '/settings/profile', label: 'Profile', icon: UserIcon },
    { path: '/settings/security', label: 'Security', icon: ShieldIcon }
  ];
  
  return (
    <Routes>
      <Route path="/dashboard" element={
        <SidebarLayout title="Dashboard" navigation={dashboardNav} />
      }>
        <Route index element={<DashboardOverview />} />
        <Route path="analytics" element={<Analytics />} />
        <Route path="reports" element={<Reports />} />
      </Route>
      
      <Route path="/settings" element={
        <SidebarLayout title="Settings" navigation={settingsNav} />
      }>
        <Route index element={<GeneralSettings />} />
        <Route path="profile" element={<ProfileSettings />} />
        <Route path="security" element={<SecuritySettings />} />
      </Route>
    </Routes>
  );
}

2. Composition with HOCs

// Higher-order layout components
function withBreadcrumbs(WrappedLayout) {
  return function BreadcrumbLayout(props) {
    const matches = useMatches();
    
    const breadcrumbs = matches
      .filter(match => match.handle?.breadcrumb)
      .map(match => ({
        label: match.handle.breadcrumb,
        path: match.pathname
      }));
    
    return (
      <div>
        <Breadcrumbs items={breadcrumbs} />
        <WrappedLayout {...props} />
      </div>
    );
  };
}

function withSidebar(WrappedLayout) {
  return function SidebarLayout(props) {
    const [isCollapsed, setIsCollapsed] = useState(false);
    
    return (
      <div className={`with-sidebar ${isCollapsed ? 'collapsed' : ''}`}>
        <Sidebar 
          isCollapsed={isCollapsed}
          onToggle={() => setIsCollapsed(!isCollapsed)}
        />
        <div className="main-area">
          <WrappedLayout {...props} />
        </div>
      </div>
    );
  };
}

// Composing layouts
const DashboardWithFeatures = withBreadcrumbs(withSidebar(DashboardLayout));

function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={<DashboardWithFeatures />}>
        {/* Nested routes */}
      </Route>
    </Routes>
  );
}

Advanced Nested Route Patterns

1. Parallel Routes

// Multiple outlets in one layout
function SplitViewLayout() {
  return (
    <div className="split-view">
      <div className="left-panel">
        <Outlet context={{ panel: 'left' }} />
      </div>
      <div className="right-panel">
        <Outlet context={{ panel: 'right' }} />
      </div>
    </div>
  );
}

// Using named outlets (conceptual - React Router doesn't support this directly)
function ParallelRoutesApp() {
  return (
    <Routes>
      <Route path="/editor" element={<SplitViewLayout />}>
        <Route index element={
          <>
            <FileExplorer outlet="left" />
            <CodeEditor outlet="right" />
          </>
        } />
        <Route path="file/:fileId" element={
          <>
            <FileExplorer outlet="left" />
            <FileEditor outlet="right" />
          </>
        } />
      </Route>
    </Routes>
  );
}

// Alternative approach using state
function SplitViewWithState() {
  const [leftContent, setLeftContent] = useState(null);
  const [rightContent, setRightContent] = useState(null);
  const location = useLocation();
  
  useEffect(() => {
    // Determine content based on route
    if (location.pathname.includes('/file/')) {
      setLeftContent(<FileExplorer />);
      setRightContent(<FileEditor />);
    } else {
      setLeftContent(<ProjectList />);
      setRightContent(<Welcome />);
    }
  }, [location]);
  
  return (
    <div className="split-view">
      <div className="left-panel">{leftContent}</div>
      <div className="right-panel">{rightContent}</div>
    </div>
  );
}

2. Modal Routes with Nested Content

// Modal system with nested routes
function ModalRoutes() {
  const location = useLocation();
  const navigate = useNavigate();
  const background = location.state?.background;
  
  return (
    <>
      <Routes location={background || location}>
        <Route path="/products" element={<ProductsLayout />}>
          <Route index element={<ProductList />} />
          <Route path=":productId" element={<ProductDetail />}>
            <Route index element={<ProductOverview />} />
            <Route path="specs" element={<ProductSpecs />} />
            <Route path="reviews" element={<ProductReviews />} />
          </Route>
        </Route>
      </Routes>
      
      {/* Modal routes */}
      {background && (
        <Routes>
          <Route path="/products/:productId" element={
            <Modal onClose={() => navigate(-1)}>
              <ProductDetail />
            </Modal>
          }>
            <Route index element={<ProductOverview />} />
            <Route path="specs" element={<ProductSpecs />} />
            <Route path="reviews" element={<ProductReviews />} />
          </Route>
        </Routes>
      )}
    </>
  );
}

3. Wizard with Nested Steps

// Complex wizard with nested steps
function WizardRoutes() {
  const [wizardData, setWizardData] = useState({});
  
  return (
    <Routes>
      <Route path="/onboarding" element={
        <WizardLayout data={wizardData} onUpdate={setWizardData} />
      }>
        <Route index element={<Navigate to="welcome" replace />} />
        
        <Route path="welcome" element={<WelcomeStep />} />
        
        <Route path="account" element={<AccountSetupLayout />}>
          <Route index element={<Navigate to="basic" replace />} />
          <Route path="basic" element={<BasicInfoStep />} />
          <Route path="credentials" element={<CredentialsStep />} />
          <Route path="verification" element={<VerificationStep />} />
        </Route>
        
        <Route path="profile" element={<ProfileSetupLayout />}>
          <Route index element={<Navigate to="personal" replace />} />
          <Route path="personal" element={<PersonalInfoStep />} />
          <Route path="preferences" element={<PreferencesStep />} />
          <Route path="avatar" element={<AvatarUploadStep />} />
        </Route>
        
        <Route path="complete" element={<CompletionStep />} />
      </Route>
    </Routes>
  );
}

function WizardLayout({ data, onUpdate }) {
  const location = useLocation();
  const navigate = useNavigate();
  
  const steps = [
    { path: 'welcome', label: 'Welcome' },
    { path: 'account', label: 'Account Setup' },
    { path: 'profile', label: 'Profile Setup' },
    { path: 'complete', label: 'Complete' }
  ];
  
  const currentStepIndex = steps.findIndex(
    step => location.pathname.includes(step.path)
  );
  
  const canNavigateToStep = (stepIndex) => {
    // Implement validation logic
    return stepIndex <= currentStepIndex + 1;
  };
  
  return (
    <div className="wizard">
      <WizardProgress steps={steps} currentStep={currentStepIndex} />
      
      <div className="wizard-content">
        <Outlet context={{ data, onUpdate }} />
      </div>
      
      <WizardNavigation
        currentStep={currentStepIndex}
        totalSteps={steps.length}
        onNext={() => navigate(steps[currentStepIndex + 1]?.path)}
        onPrevious={() => navigate(steps[currentStepIndex - 1]?.path)}
        canNavigateToStep={canNavigateToStep}
      />
    </div>
  );
}

State Management in Nested Routes

1. Shared State Pattern

// State management across nested routes
function StateProvider({ children }) {
  const [sharedState, setSharedState] = useState({
    filters: {},
    sortBy: 'date',
    viewMode: 'grid'
  });
  
  const updateState = (updates) => {
    setSharedState(prev => ({ ...prev, ...updates }));
  };
  
  return (
    <SharedStateContext.Provider value={{ sharedState, updateState }}>
      {children}
    </SharedStateContext.Provider>
  );
}

function ProductsLayout() {
  return (
    <StateProvider>
      <div className="products-layout">
        <ProductsHeader />
        <Outlet />
      </div>
    </StateProvider>
  );
}

function ProductList() {
  const { sharedState, updateState } = useContext(SharedStateContext);
  const [products, setProducts] = useState([]);
  
  useEffect(() => {
    fetchProducts(sharedState.filters, sharedState.sortBy)
      .then(setProducts);
  }, [sharedState.filters, sharedState.sortBy]);
  
  return (
    <div className={`product-list ${sharedState.viewMode}`}>
      <FilterBar 
        filters={sharedState.filters}
        onFilterChange={(filters) => updateState({ filters })}
      />
      <ViewModeToggle
        mode={sharedState.viewMode}
        onChange={(viewMode) => updateState({ viewMode })}
      />
      <div className="products">
        {products.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

2. Route-based State Persistence

// Persist state across route changes
function usePersistentState(key, initialValue) {
  const location = useLocation();
  const navigate = useNavigate();
  
  // Get state from URL or localStorage
  const getInitialState = () => {
    const urlState = new URLSearchParams(location.search).get(key);
    if (urlState) {
      try {
        return JSON.parse(decodeURIComponent(urlState));
      } catch (e) {
        console.error('Failed to parse URL state:', e);
      }
    }
    
    const savedState = localStorage.getItem(`route_state_${key}`);
    if (savedState) {
      try {
        return JSON.parse(savedState);
      } catch (e) {
        console.error('Failed to parse saved state:', e);
      }
    }
    
    return initialValue;
  };
  
  const [state, setState] = useState(getInitialState);
  
  // Update URL and localStorage when state changes
  useEffect(() => {
    const params = new URLSearchParams(location.search);
    params.set(key, encodeURIComponent(JSON.stringify(state)));
    
    navigate(`${location.pathname}?${params.toString()}`, { replace: true });
    localStorage.setItem(`route_state_${key}`, JSON.stringify(state));
  }, [state, key, location.pathname, navigate]);
  
  return [state, setState];
}

// Using persistent state in nested routes
function FilterableList() {
  const [filters, setFilters] = usePersistentState('filters', {
    category: 'all',
    priceRange: [0, 1000],
    inStock: true
  });
  
  return (
    <div>
      <FilterPanel filters={filters} onChange={setFilters} />
      <Outlet context={{ filters }} />
    </div>
  );
}

Performance Optimization

1. Lazy Loading Nested Routes

import { lazy, Suspense } from 'react';

// Lazy load nested route components
const DashboardOverview = lazy(() => import('./pages/Dashboard/Overview'));
const Analytics = lazy(() => import('./pages/Dashboard/Analytics'));
const Reports = lazy(() => import('./pages/Dashboard/Reports'));
const ReportDetails = lazy(() => import('./pages/Dashboard/Reports/Details'));

function LazyBoundary({ children }) {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      {children}
    </Suspense>
  );
}

function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={<DashboardLayout />}>
        <Route index element={
          <LazyBoundary>
            <DashboardOverview />
          </LazyBoundary>
        } />
        <Route path="analytics" element={
          <LazyBoundary>
            <Analytics />
          </LazyBoundary>
        } />
        <Route path="reports" element={
          <LazyBoundary>
            <Outlet />
          </LazyBoundary>
        }>
          <Route index element={<Reports />} />
          <Route path=":reportId" element={
            <LazyBoundary>
              <ReportDetails />
            </LazyBoundary>
          } />
        </Route>
      </Route>
    </Routes>
  );
}

2. Memoization in Nested Components

// Optimize nested route rendering
const MemoizedSidebar = React.memo(function Sidebar({ items, activeItem }) {
  return (
    <nav className="sidebar">
      {items.map(item => (
        <NavLink
          key={item.id}
          to={item.path}
          className={({ isActive }) => 
            `nav-item ${isActive ? 'active' : ''}`
          }
        >
          {item.label}
        </NavLink>
      ))}
    </nav>
  );
});

const MemoizedHeader = React.memo(function Header({ title, user }) {
  return (
    <header>
      <h1>{title}</h1>
      <UserMenu user={user} />
    </header>
  );
});

function OptimizedLayout() {
  const { user } = useAuth();
  const location = useLocation();
  
  // Memoize navigation items
  const navItems = useMemo(() => [
    { id: 1, path: '/dashboard', label: 'Dashboard' },
    { id: 2, path: '/analytics', label: 'Analytics' },
    { id: 3, path: '/reports', label: 'Reports' }
  ], []);
  
  // Memoize title computation
  const pageTitle = useMemo(() => {
    const path = location.pathname.split('/').pop();
    return path ? path.charAt(0).toUpperCase() + path.slice(1) : 'Home';
  }, [location.pathname]);
  
  return (
    <div className="layout">
      <MemoizedHeader title={pageTitle} user={user} />
      <div className="content">
        <MemoizedSidebar items={navItems} />
        <main>
          <Outlet />
        </main>
      </div>
    </div>
  );
}

Practice Exercises

Exercise 1: Build a Multi-level Admin Panel

Create an admin panel with:

  • Dashboard with multiple sections
  • User management with nested CRUD operations
  • Content management with categories and items
  • Settings with multiple configuration sections

Exercise 2: Create a Document Editor

Build a document editor with nested routes for:

  • Document list view
  • Document editor with tabs (content, metadata, preview)
  • Nested comments section
  • Version history with diffs

Exercise 3: Implement a Multi-step Checkout

Create a checkout process with:

  • Cart review
  • Shipping information (with address validation)
  • Payment details (with multiple payment methods)
  • Order confirmation

Nested Routes Best Practices

1. Keep Route Hierarchy Logical

// Good: Clear hierarchy
/products
/products/:id
/products/:id/reviews
/products/:id/reviews/:reviewId

// Bad: Flat structure
/products
/product-details
/product-reviews
/review-details

2. Use Index Routes Effectively

<Route path="/dashboard" element={<DashboardLayout />}>
  {/* Default content when visiting /dashboard */}
  <Route index element={<DashboardHome />} />
  <Route path="analytics" element={<Analytics />} />
</Route>

3. Optimize Context Usage

// Split contexts to prevent unnecessary re-renders
const UserContext = createContext();
const ThemeContext = createContext();
const DataContext = createContext();

// Instead of one large context
const AppContext = createContext();

Key Takeaways