Nested Layouts
Your dashboard has a sidebar. Every page under /dashboard/* needs it. You copy-paste the sidebar into each page. Then the design changes. Now you're updating 12 files.
Nested layouts solve this: define the sidebar once, and every child route inherits it automatically. They persist across route changes and compose automatically.
VIDEO PLACEHOLDER · 3-4 min · EVERGREEN · Medium Priority
Layout Composition: How Nested Layouts Stack
Animated visualization of how layouts compose - root layout wraps section layout wraps page. Shows what persists vs re-renders during navigation, and why this matters for state preservation and performance.
Outcome
A nested layout for a section that overrides root layout chrome.
Fast Track
- Add a section
layout.tsxin a nested folder. - Move relevant chrome to the section layout.
- Verify composition and slotting (how children are placed within layout wrappers).
Hands-On Exercise 2.4
Build nested layouts that compose persistent UI across route segments.
Requirements:
- Create a nested layout under a route group or segment.
- Show different header/footer than root.
- Keep child routing intact.
- Demonstrate persistence across route changes within the section.
Implementation hints:
- layout.tsx in each segment: Creates nested composition automatically.
- Layouts persist: Don't re-render when navigating between child routes.
- Automatic deduplication: Next.js ensures layouts only render once per navigation.
- Children prop pattern: Layout receives
childrenand wraps it with section-specific UI. - Layouts with params: If your layout needs params (rare but possible), remember params are async Promises. Layout must be async and await params.
- Avoid duplicating providers if not needed.
- Keep the layout lean; minimize data fetching here.
Layouts persist across route changes within their segment. State is maintained when navigating between child pages, improving performance and user experience.
Try It
- Navigate within the section; verify persistent chrome.
Commit & Deploy
git add -A
git commit -m "feat(core): add nested layout for section chrome"
git push -u origin feat/core-nested-layoutDone-When
- Navigate from
/dashboardto/dashboard/analytics: sidebar remains visible and does not flash/reload - Navigate from
/dashboard/analyticsto/dashboard/settings: sidebar remains visible (verify by watching for any flicker) - Navigate from
/to/about: marketing header/footer persists without reload - Compare
/(marketing) vs/dashboard: different chrome visible (header/footer vs sidebar) - Visit
/about(not/marketing/about): route group does not add segment to URL - Open DevTools Network tab, navigate between dashboard pages: layout.tsx JavaScript does not re-fetch
Solution
Solution
File Structure
apps/web/src/app/
├── layout.tsx # Root layout (html, body, global providers)
├── (marketing)/
│ ├── layout.tsx # Marketing layout (header, footer, nav)
│ ├── page.tsx # Home page
│ ├── about/page.tsx
│ └── pricing/page.tsx
└── dashboard/
├── layout.tsx # Dashboard layout (sidebar, different chrome)
├── page.tsx # Dashboard overview
├── analytics/page.tsx
└── settings/page.tsx
How Layout Composition Works
┌─────────────────────────────────────────────────────────────┐
│ Root Layout (layout.tsx) │
│ ├── <html>, <body>, global providers │
│ │ │
│ │ ┌─────────────────────────────────────────────────────┐│
│ │ │ Marketing Layout ((marketing)/layout.tsx) ││
│ │ │ ├── Header with nav ││
│ │ │ ├── {children} ← Page content renders here ││
│ │ │ └── Footer ││
│ │ └─────────────────────────────────────────────────────┘│
│ │ │
│ │ ┌─────────────────────────────────────────────────────┐│
│ │ │ Dashboard Layout (dashboard/layout.tsx) ││
│ │ │ ├── Sidebar (persists across routes) ││
│ │ │ └── {children} ← Page content renders here ││
│ │ └─────────────────────────────────────────────────────┘│
│ │ │
└─────────────────────────────────────────────────────────────┘
Root Layout
// Root layout: wraps ALL pages, provides html/body structure
// Keep this minimal: no section-specific chrome here
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "Next.js Foundations",
description: "Learning Next.js patterns",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="min-h-screen bg-white text-gray-900">
{/* Global providers would go here (theme, auth, etc.) */}
{children}
</body>
</html>
);
}Marketing Layout (Route Group)
// Marketing section layout: header, footer, nav
// (marketing) is a route group - doesn't affect URL structure
// Routes: /, /about, /pricing (NOT /marketing/about)
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen">
{/* Marketing header - persists across all marketing pages */}
<header className="mb-8 border-b py-4">
<nav className="mx-auto flex max-w-4xl gap-4 px-4">
<a href="/" className="font-semibold">
Home
</a>
<a href="/about" className="text-gray-600 hover:text-gray-900">
About
</a>
<a href="/pricing" className="text-gray-600 hover:text-gray-900">
Pricing
</a>
{/* Link to dashboard (different layout) */}
<a
href="/dashboard"
className="ml-auto text-blue-600 hover:text-blue-800"
>
Dashboard →
</a>
</nav>
</header>
{/* Page content renders here */}
<main className="mx-auto max-w-4xl px-4">{children}</main>
{/* Marketing footer - persists across all marketing pages */}
<footer className="mx-auto mt-8 max-w-4xl border-t px-4 py-4 text-gray-500 text-sm">
© 2026 Next.js Foundations
</footer>
</div>
);
}Dashboard Layout (Nested Segment)
// Dashboard layout: sidebar navigation, different chrome than marketing
// This layout PERSISTS when navigating between /dashboard/* routes
// The sidebar doesn't re-render - only the main content area changes
import Link from "next/link";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex min-h-screen">
{/* Sidebar - persists across all dashboard routes */}
<aside className="w-64 border-r bg-gray-50 p-4">
<h2 className="mb-4 font-bold text-lg">Dashboard</h2>
<nav className="space-y-2">
<Link
href="/dashboard"
className="block rounded px-3 py-2 text-gray-700 hover:bg-gray-200"
>
Overview
</Link>
<Link
href="/dashboard/analytics"
className="block rounded px-3 py-2 text-gray-700 hover:bg-gray-200"
>
Analytics
</Link>
<Link
href="/dashboard/settings"
className="block rounded px-3 py-2 text-gray-700 hover:bg-gray-200"
>
Settings
</Link>
</nav>
<div className="mt-8 border-t pt-4">
{/* Link back to marketing section */}
<Link
href="/"
className="text-gray-500 text-sm hover:text-gray-700"
>
← Back to site
</Link>
</div>
<div className="mt-4">
<p className="text-gray-400 text-xs">
This sidebar persists when navigating between dashboard pages.
</p>
</div>
</aside>
{/* Main content area - children change on navigation */}
<main className="flex-1 p-8">{children}</main>
</div>
);
}Dashboard Pages
// Dashboard overview page
// When navigating here from /dashboard/analytics, only this component re-renders
// The sidebar layout persists (no flicker, state preserved)
export default function DashboardPage() {
return (
<div>
<h1 className="mb-4 font-bold text-3xl">Dashboard Overview</h1>
<p className="mb-6 text-gray-600">
Welcome to your dashboard. Navigate between pages using the sidebar.
Notice how the sidebar persists across route changes.
</p>
<div className="grid grid-cols-3 gap-4">
<div className="rounded-lg border bg-blue-50 p-4">
<h3 className="font-semibold text-blue-800">Users</h3>
<p className="font-bold text-2xl text-blue-600">1,234</p>
</div>
<div className="rounded-lg border bg-green-50 p-4">
<h3 className="font-semibold text-green-800">Revenue</h3>
<p className="font-bold text-2xl text-green-600">$45,678</p>
</div>
<div className="rounded-lg border bg-purple-50 p-4">
<h3 className="font-semibold text-purple-800">Orders</h3>
<p className="font-bold text-2xl text-purple-600">567</p>
</div>
</div>
</div>
);
}// Analytics page - demonstrates layout persistence
// Try adding console.log in the layout to verify it doesn't re-run
export default function AnalyticsPage() {
return (
<div>
<h1 className="mb-4 font-bold text-3xl">Analytics</h1>
<p className="mb-6 text-gray-600">
This is the analytics page. The sidebar layout persisted during
navigation.
</p>
<div className="rounded-lg border p-6">
<h2 className="mb-4 font-semibold text-xl">Traffic Overview</h2>
<div className="flex h-64 items-center justify-center rounded bg-gray-100">
<span className="text-gray-400">[Chart placeholder]</span>
</div>
</div>
</div>
);
}// Settings page - form state would persist across sibling navigations
// If you had a form here with unsaved changes, navigating to /analytics
// and back would preserve those changes (because the layout persists)
export default function SettingsPage() {
return (
<div>
<h1 className="mb-4 font-bold text-3xl">Settings</h1>
<p className="mb-6 text-gray-600">
Configure your dashboard settings. The sidebar persists during
navigation.
</p>
<div className="space-y-6">
<div className="rounded-lg border p-4">
<h2 className="mb-2 font-semibold">Notifications</h2>
<label className="flex items-center gap-2">
<input type="checkbox" className="rounded" />
<span>Email notifications</span>
</label>
</div>
<div className="rounded-lg border p-4">
<h2 className="mb-2 font-semibold">Theme</h2>
<select className="rounded border px-3 py-2">
<option>Light</option>
<option>Dark</option>
<option>System</option>
</select>
</div>
</div>
</div>
);
}Testing Layout Persistence
- Navigate Dashboard: Click between Overview → Analytics → Settings
- Watch Network Tab: Layout CSS/JS doesn't reload between dashboard pages
- Add State Test: Add a counter to the sidebar, verify it persists across navigation
- Compare Layouts: Visit
/(marketing) vs/dashboardto see different chrome - Route Group: Confirm
/aboutworks (NOT/marketing/about)
Key Patterns
- Route Groups:
(marketing)folder organizes files without affecting URLs - Segment Layouts:
dashboard/layout.tsxapplies to all/dashboard/*routes - Persistence: Layouts don't re-render on child navigation (state preserved)
- No Provider Duplication: Put providers in root layout, not every nested layout
References
Was this helpful?