Lesson 84·M10 App Router
Layouts & Nested Layouts
OpenLearning Objectives
By the end of this lesson you should be able to:
- Say what a layout is: a persistent shell that wraps a segment and its subtree
- Explain why a layout does not re-render when you navigate between the pages under it
- Draw the nested-layout tree for a multi-segment route and predict what renders where
- State the root-layout requirement and the one thing only it owns (
<html>/<body>) - Compare layouts against the old
_app/_documentapproach and againsttemplate.tsx - Decide when a shared shell belongs in a layout vs a component
One-Line Definition
A layout is a React component that renders once and wraps its segment and everything below it, persisting across navigations — it does not re-render when the page inside it changes.
Mental Model
A layout is the frame of a painting. You hang the frame once; the artwork inside gets swapped when you navigate. The frame does not get rebuilt, repainted, or refetched for each new artwork — only the canvas inside changes.
┌─────────────────────────────────────┐
│ app/layout.tsx │ ← frame, mounted once
│ ┌─────────────────────────────────┐│
│ │ app/dashboard/layout.tsx ││ ← nested frame, mounted once
│ │ ┌─────────────────────────────┐││
│ │ │ page.tsx ← swapped on nav │││ ← the artwork
│ │ └─────────────────────────────┘││
│ └─────────────────────────────────┘│
└─────────────────────────────────────┘
The frame persists; the artwork changes.
Visual Flow
app/
layout.tsx root layout ── mounts on first load
dashboard/
layout.tsx nested layout ── mounts on first visit
page.tsx /dashboard
settings/
page.tsx /dashboard/settings
navigate /dashboard → /dashboard/settings
root layout stays mounted, does NOT re-render
dashboard layout stays mounted, does NOT re-render
page unmounts → new page mounts
Only the innermost page swaps. Every ancestor layout keeps its DOM, its state, and its server-rendered output untouched.
How It Works
A layout is a component that receives children — the content one level deeper — and wraps
it. Next.js composes the tree for a URL by nesting every layout on the path from the root
down to the page:
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<nav className="sidebar">
<Link href="/dashboard">Overview</Link>
<Link href="/dashboard/settings">Settings</Link>
</nav>
<main>{children}</main>
);
}
/dashboard/settings renders as:
<RootLayout> ← app/layout.tsx
<DashboardLayout> ← app/dashboard/layout.tsx
<main><SettingsPage/></main> ← app/dashboard/settings/page.tsx
</DashboardLayout>
</RootLayout>
Two facts to internalize:
- A layout must return JSX that accepts
children. A layout that dropschildrensilently hides every page beneath it. app/layout.tsxis required and is the only layout allowed to render<html>and<body>. Nested layouts render their own UI, never the document tags — a second<html>breaks hydration.
Real Project Usage
| Need | Where it goes |
|---|---|
Global shell: <html>, nav, footer | app/layout.tsx (required, renders <html>/<body>) |
| Auth-gated area | app/dashboard/layout.tsx — the guard runs before any dashboard page renders |
| Per-section chrome (sidebar, tabs) | the section's layout.tsx |
| Stable header that never flashes on nav | a layout (it never re-renders) |
| Content that changes per navigation | the page (it always re-renders) |
A canonical nested pair — a docs section with its own chrome:
// app/docs/layout.tsx
export default function DocsLayout({ children }: { children: React.ReactNode }) {
return (
<div className="docs">
<aside>TOC</aside>
<article>{children}</article>
</div>
);
}
/docs/quickstart and /docs/api both render inside the same TOC + article frame;
navigating between them does not re-render the aside.
The sidebar belongs in the layout, not in every page — that is what "layouts do not re-render on navigation" buys you.
Interview Explanation
A layout is a React component that wraps a segment of the tree and persists across navigations. It receives
childrenand renders them inside its own shell — the shell of the root layout is the<html>/<body>document. When you navigate between two pages under the same layout, only the page re-renders; the layout keeps its state, its DOM and its effects untouched. Nested layouts compose:app/dashboard/layout.tsxwrapsapp/dashboard/settings/page.tsxinside the root layout, so a URL renders through every ancestor frame.
That's the 30-second answer: persistent shell, receives children, composes down the segment tree.
Senior-Level Insights
- "Does not re-render" is a guarantee about client-side navigation. On a hard reload or a server round-trip, the server re-renders layouts too — the persistence is a client-navigation property, not "the server caches my shell".
- State placement follows the frame. Anything that must survive a page swap lives in the layout: the sidebar's open state, the scroll position, the active tab. Anything that must reset on navigation lives in the page. That split is the layout design.
- Nested layouts are how you scope authority. A dashboard layout can enforce auth and fetch the user once; deeper layouts can't accidentally bypass it. The tree structure encodes who is allowed to render what.
- Suspense boundaries interact with layouts. A
loading.tsx(Lesson 83) is scoped to its segment, so a slow page streams inside the layout frame while the shell stays interactive — layouts and streaming are what make the App Router feel instant.
Common Mistakes
- State in the layout resets because you remount it. A layout re-mounts when its
segment changes in the tree — adding or removing a layout above it.
keyand conditional-render mistakes upstream also force remounts. "Layouts persist" holds only when the layout stays in the tree. - Rendering
<html>in a nested layout. Only the root layout owns the document tags; a nested<html>duplicates the document and breaks hydration. - Dropping
children. A layout that renders a fixed sidebar and forgets{children}silently renders every nested page invisible. The most common layout "bug" is a missingchildren. - Putting per-page data in the layout. Data that only the page needs belongs in the page; a layout that awaits the same query for every child re-runs it on every child navigation (unless cached, Lesson 90).
- Reaching for
template.tsxby default. Atemplatere-mounts on every navigation — the opposite of a layout. Use it only when the shell should reset (e.g. re-running a page transition animation).
Best Practices
✅ Put shared, navigation-stable chrome in layouts — header, sidebar, footer
✅ Render <html>/<body> only in the root app/layout.tsx
✅ Keep {children} in every layout — dropping it hides the whole subtree
✅ Do auth and user fetching in a layout that scopes an area, once
✅ Give a nested section its own layout the moment it gets its own chrome
❌ Don't put per-page data fetches in a layout that wraps many pages
❌ Don't use template.tsx when you want persistence — templates are the reset variant
Interview Questions
Q1. What is a layout in the App Router, and how does it differ from a page?
A layout is a component that wraps a segment and its subtree, receiving
childrenand rendering them inside a persistent shell. A page is the leaf that changes on navigation. Navigate between two pages under the same layout and only the page re-renders — the layout keeps its state and DOM.
Q2. Why don't layouts re-render on navigation?
Because the layout stays in the tree while only the page beneath it changes. On client-side navigation the router swaps the page's content inside the already-mounted layout frame; there is no new render of the frame, so its state and effects survive. That's what makes the navigation shell — header, sidebar — feel instant and continuous.
Q3. What is the root layout, and why is it special?
app/layout.tsxis required and is the outermost layout. It's the only place that renders<html>and<body>— the actual document. Every other layout renders UI inside that document. Without it the app has no document to hydrate.
Q4. How do nested layouts work?
Each segment can add a layout that wraps everything below it. For
/dashboard/settings, the root layout wraps the dashboard layout wraps the settings page. Rendering is composition: each layout renderschildren, which is the next layout or page down the tree.
Senior follow-up: Your header flickers on every route change. Where is the bug?
The header is being re-rendered, so it's not in a layout where it belongs — or the layout is being remounted. I'd check three things: is the header rendered in a layout and not in each page; does anything upstream conditionally mount that layout; and is the data it renders cached so a navigation doesn't refetch it. The fix is moving the header into the persistent frame and keeping its data out of per-page fetches.
Follow-up Questions
When would you choose template.tsx over layout.tsx?
When the shell must reset on every navigation — page-transition animations, analytics that must re-run, a spotlight that should restart. A template re-mounts its children each time; a layout persists them.
Can a layout be a Server Component?
Yes, and by default it is (Lesson 86). A server layout fetches once and passes plain data down; client interactivity lives in islands inside it. A layout that needs client state (e.g. a sidebar toggle) is the exception that opts in with
use client.
Does the layout re-fetch its data when you navigate?
The layout itself doesn't re-render, so it doesn't re-run its fetches on client navigation — unless the data is marked dynamic or the navigation invalidates its cache (Lesson 90). That's precisely why layouts are the right home for data every page in the section shares.
Comparison Table
layout.tsx | page.tsx | template.tsx | |
|---|---|---|---|
| Role | Persistent shell | The current view | Shell that resets |
| Re-renders on navigation | ❌ | ✅ always | ✅ always |
| Keeps state across nav | ✅ | ❌ | ❌ |
| Root requirement | ✅ required, owns <html>/<body> | required for a route | optional |
Receives children | ✅ | ❌ renders content | ✅ |
| Old-router equivalent | _app.tsx/_document.tsx (global only) | a pages/*.tsx file | none |
Code Example
A complete root + nested layout composition, runnable in shape:
// app/layout.tsx — the document shell
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<header>Acme</header>
{children}
</body>
</html>
);
}
root frame: <html><body><header>Acme</header>{…}</body></html>
// app/shop/layout.tsx — nested frame with its own chrome
export default function ShopLayout({ children }: { children: React.ReactNode }) {
return (
<div className="shop">
<nav>Categories</nav>
<main>{children}</main>
</div>
);
}
/shop/checkout renders:
<html><body><header>Acme</header>
<div class="shop"><nav>Categories</nav><main><CheckoutPage/></main></div>
</body></html>Performance Notes
- Layouts are the biggest free win in the router. They render once and never re-render on navigation, so all their work — server render, data fetch, hydration — is paid once, not per route. This is the lesson's core fact in performance terms.
- Server layouts avoid duplicate work. A layout that awaits the current user or the section's nav runs once per navigation, not per page. Without the cache layer (Lesson 90), that's the cheapest place to share data across a section.
- The persistence is client-side. On hard reloads the server re-renders the whole tree, so a heavy layout costs a full render on every fresh visit. Mitigate with caching and streaming, not by shrinking the layout.
- Templates cost more, on purpose. Each navigation re-mounts a template and its subtree, so avoid them on hot paths. Layouts are free; templates are a deliberate spend.
- Don't over-tune. A small app with one layout needs nothing more. Nested layouts earn their complexity when sections genuinely have distinct shells and authority scopes.
Debugging Scenarios
Scenario 1: "My pages are blank — the sidebar renders but the content doesn't."
The layout is missing {children}. A layout that renders a fixed frame and never includes
children hides every nested page. Add {children} inside the frame.
Scenario 2: "The header flashes and loses state on every navigation."
The header is not in a layout, or the layout is being remounted. Check where the header is
rendered — it belongs in a layout above the changing pages — and whether anything
conditionally mounts that layout (a key change, a dynamic import, a route group
mismatch). Remounting destroys the persistence guarantee.
Scenario 3: "Duplicate <html> tags appear in the DOM."
A layout other than the root renders the document tags. Nested layouts must only render
their own UI — <html>/<body> is the root layout's exclusive job.
Scenario 4: "Layout data is stale after I update the database."
The layout isn't re-rendering on navigation, so its fetch isn't re-running either — the shell was cached (Lesson 90). Revalidate the segment or mark the data dynamic; this is the trade side of "layouts don't re-render".
Quick Revision Notes
- Layout = persistent shell wrapping a segment; receives
children; renders once - Layouts do not re-render on client-side navigation — only the page beneath swaps
- Nested layouts compose: every ancestor layout wraps the ones below it
app/layout.tsxis required and is the only file that renders<html>/<body>- Dropping
{children}hides the entire subtree — the classic layout bug template.tsxis the reset variant: re-mounts on every navigation- Old router:
_app/_documentgave one global shell; the App Router gives one per segment - Server layouts run once per navigation — the free place to share section data
Cheat Sheet
layout = frame, mounts once, never re-renders on navigation
page = artwork, always re-renders
app/
layout.tsx → <html><body> … </body></html> (required, owns document)
dashboard/
layout.tsx → sidebar + {children} (persists for the section)
page.tsx → /dashboard (swaps on nav)
settings/
page.tsx → /dashboard/settings
composition: <RootLayout><DashboardLayout><Page/></DashboardLayout></RootLayout>
navigate page → page: layouts untouched, page remounts
hard reload: server re-renders everything
template.tsx = layout that re-mounts children on every navigation
rules:
✅ {children} in every layout ❌ <html> outside the root layout
✅ chrome in layouts ❌ per-page data in a section layout
✅ state that survives → layout ❌ template when you want persistenceKey Takeaways
Recap
- A layout is a persistent frame: it wraps a segment and its subtree and survives navigation
- Layouts do not re-render on client-side navigation — only the page inside them changes
- Nested layouts compose: each level renders
children, down to the page app/layout.tsxis required and uniquely owns<html>/<body>- A layout without
{children}renders its subtree invisible template.tsxis the reset counterpart — re-mounts on every navigation- The App Router replaced
_app/_document's single shell with one layout per segment - "Where does state survive?" and "layouts don't re-render" are the same question
Check your understanding
Answer each one out loud. Then rate yourself honestly.
0/7Define a layout in one sentence. What does it receive, and what does it do with it?
Why does navigating between two pages under the same layout not re-render the layout?
Draw the render tree for /dashboard/settings with a root layout and a dashboard layout.
Which file is required and the only one allowed to render <html> and <body>?
What happens if a layout omits {children}?
When is a template.tsx the right choice over a layout — and what does it cost?
A header must keep its scroll position across navigations. Where does it live, and why?
What's Next
Lesson 85 — Dynamic Routes. [slug] segments, generateStaticParams, params in
server components, and catch-all segments — how one file serves an entire tree of URLs.