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
- Nested routes create hierarchical UI structures
- Use Outlet component to render child routes
- Pass data through outlet context
- Implement layout composition for reusability
- Consider performance with lazy loading
- Manage state effectively across nested routes