#Pages and layouts
AddedThe App Router uses file-system routing: every folder under app/ is a route segment, and the folder path is the URL path. A folder on its own isn't a page, though. It becomes reachable only when it contains a page.tsx, the UI for exactly that URL. A layout.tsx in the same folder wraps that page and every page below it, receiving them as children. Layouts nest: the root layout wraps the shop layout, which wraps the shirts page.
The mental model is picture frames on a wall. Each layout is a frame, the page is the photo inside the innermost frame. When you move from /shop/shirts to /shop/hats, Next.js doesn't take the frames down: it swaps the photo. The root layout and the shop layout stay mounted, keep their React state (a cart counter, an open menu, a half-typed search) and are not re-rendered or re-fetched. Only the part of the tree below the deepest layout the two URLs share changes. That one rule explains most of the App Router's navigation behaviour.
The other files you'll meet (loading, error, not-found, template, default, route) are covered later in this chapter and in Project structure and special files. Everything in a segment folder that isn't one of these special file names is not routable, so components, helpers and CSS can live next to the page that uses them.
import { CartCounter } from "./cart-counter";
export default function ShopLayout({ children }: LayoutProps<"/shop">) {
return (
<section id="shop-layout">
<aside>
<h2>Shop</h2>
<CartCounter />
</aside>
<main>{children}</main>
</section>
);
}- This is
app/shop/layout.tsx, the layout for theshopsegment. The app around it (all seven files are below) has four folders with apage.tsx:app/,shop/,shop/shirts/andshop/hats/. So it has exactly four public URLs:/,/shop,/shop/shirtsand/shop/hats.cart-counter.tsxsits inshop/too, but it isn't a special file name, so it's only a module this layout imports;/shop/cart-counterdoesn't exist. - A request for
/shop/shirtsrenders the chain of layouts from the root down to the matching page:RootLayout(app/layout.tsx) gets<ShopLayout>as itschildren, andShopLayoutgets<ShirtsPage>as itschildren, which it places inside<main>. You never import a page into a layout; the router passes it in. - The props are typed with
LayoutProps<"/shop">, a global helper Next.js generates from your folders duringnext dev/next build(no import). The root layout usesLayoutProps<"/">and is the one that must render<html>and<body>. CartCounteris a Client Component ("use client") rendered inside this Server Component. It holdsitemsinuseState, which is browser state that only survives while the component stays mounted.- Click "Cart" three times, then click the Hats link. The URL becomes
/shop/hatsand the<h1>changes, but the button still saysCart: 3:ShopLayoutis shared by both URLs, so the router left it alone and swapped only the page in itschildrenslot. That's layout persistence, and the observation below shows it happening.
The rest of the app
The root layout puts a nav of <Link>s around every page; the pages are one-liners so the only state in the app is the cart counter.
import Link from "next/link";
import "./globals.css";
export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html lang="en">
<body>
<nav id="site-nav">
<Link href="/">Home</Link> · <Link href="/shop">Shop</Link> ·{" "}
<Link href="/shop/shirts">Shirts</Link> · <Link href="/shop/hats">Hats</Link>
</nav>
{children}
</body>
</html>
);
}export default function HomePage() {
return <h1>Home</h1>;
}"use client";
import { useState } from "react";
export function CartCounter() {
const [items, setItems] = useState(0);
return (
<button id="add" onClick={() => setItems(items + 1)}>
Cart: {items}
</button>
);
}export default function ShopPage() {
return <h1>All products</h1>;
}export default function ShirtsPage() {
return <h1 id="shirts">Shirts</h1>;
}export default function HatsPage() {
return <h1 id="hats">Hats</h1>;
}What the build and the browser showed
The build confirms that all four pages (and the built-in /_not-found) are plain static files:
Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /shop
├ ○ /shop/hats
└ ○ /shop/shirts
○ (Static) prerendered as static contentHere is what headless Chrome showed against next start for that app: the cart was clicked three times on /shop/shirts, a marker attribute was put on the #shop-layout element, and then the nav links were clicked in turn (Hats, then Home, then Shirts). (hidden) means the element is in the DOM but not visible.
on /shop/shirts cart "Cart: 3" h1s: Shirts
click Hats → /shop/hats cart "Cart: 3" same #shop-layout node (marker kept) h1s: Hats, Shirts (hidden)
network requests for the click: none (the static page was already prefetched)
click Home → / #shop-layout still in the DOM, not visible h1s: Home, Hats (hidden), Shirts (hidden)
click Shirts → /shop/shirts cart "Cart: 3" same #shop-layout node (marker kept) h1s: Shirts, Hats (hidden), Home (hidden)The first two lines are the classic App Router behaviour. The last two are new: even after leaving the shop section entirely, coming back found the same DOM node with the cart still at 3, and the pages you left were still in the document, hidden. That's Next.js 16 with Cache Components (cacheComponents: true, the default for new apps) hiding routes you leave with React's <Activity> instead of unmounting them.
With Cache Components on, the router doesn't unmount the route you navigate away from. It wraps routes in React's <Activity> and switches them to hidden (the DOM stays, with display: none; effects are cleaned up as if unmounted, state is kept). The bundled guide "Preserving UI state" says Next.js keeps up to 3 routes this way and evicts the oldest beyond that. Going back, or clicking a link to a route that is still kept, shows it again with its state, scroll position and form inputs intact.
To see what an older app does, the same seven files were built with an empty next.config.ts (no cacheComponents, so the pre-16 behaviour) and the same clicks replayed:
on /shop/shirts cart "Cart: 3" h1s: Shirts
click Hats → /shop/hats cart "Cart: 3" same #shop-layout node h1s: Hats
click Home → / #shop-layout not in the DOM h1s: Home
click Shirts → /shop/shirts cart "Cart: 0" a new #shop-layout node h1s: ShirtsSo the rule interviewers expect, and the rule in every Next.js 13–15 codebase, is: a layout keeps its state while you navigate between pages it wraps, and is unmounted (state lost) when you navigate to a URL outside it; the page is unmounted on every navigation. With Cache Components, add: "…unless the route is still one of the last few kept by Activity, in which case it comes back exactly as you left it." If something must reset each time, don't rely on unmounting; see Templates vs layouts.
Because a layout isn't re-rendered when you move between its child pages, it is deliberately cut off from things that change per page:
- No
searchParams. Onlypage.tsxreceives thesearchParamsprop. A layout that read?q=would show a stale value after the next navigation, so it isn't given one. Read search params in a Client Component withuseSearchParams()(Client Components do re-render on navigation). - No pathname. Same reason. An active-link nav is a Client Component using
usePathname(), imported into the layout. - No access to its children's data. A layout can't pass props to
children, and the page can't pass data up. If both need the current user, both call the same data function and let request memoisation or Reactcache()dedupe it (chapter 4, Sharing data between layouts and pages). paramsonly for segments at or above it.app/shop/[category]/layout.tsxgetsparams: Promise<{ category: string }>; it can't see a[slug]below it. UseuseSelectedLayoutSegment()in a Client Component to read the active child segment.
When a segment folder has several special files, Next.js nests them in a fixed order. Knowing it answers questions like "does loading.tsx cover a slow layout?" (no) or "does error.tsx catch errors in the layout next to it?" (no):
| Rendered as | File | What it is |
|---|---|---|
| outermost | layout.tsx | the frame; persists |
| ↓ | template.tsx | like a layout, but re-created on navigation |
| ↓ | error.tsx | a React error boundary |
| ↓ | loading.tsx | a <Suspense> boundary with your fallback |
| ↓ | not-found.tsx | the boundary for notFound() |
| innermost | page.tsx or a nested layout.tsx | the content, or the next segment down |
Because error.tsx and loading.tsx sit inside the layout of the same folder, they can't catch an error thrown by that layout or show a fallback while that layout waits; the parent segment's boundaries do that.
- It must exist (in
app/or, with route groups, one per top-level group) and must render<html>and<body>. - Don't hand-write
<head>,<title>or<meta>there: exportmetadataorgenerateMetadataand Next.js manages<head>(chapter 10). - In Next.js 15 and later the
paramsprop of every layout and page is a Promise; Next.js 14 and earlier passed a plain object. Next.js 15 still allowed synchronous access with a warning; 16 removed it.
With the app above (Cache Components on), you're on /shop/shirts and click the Hats link. Which of RootLayout, ShopLayout, CartCounter, ShirtsPage and HatsPage render again, and what does the cart show?
Show answer
Only HatsPage renders (it's new); RootLayout, ShopLayout and CartCounter are untouched, ShirtsPage is hidden, and the cart still shows its old count.
- The router compares the old and new URL segment by segment:
/matches,shopmatches,shirtsvshatsdiffers. The deepest shared layout isShopLayout. - Everything at and above
ShopLayoutis reused as it is: no server render, no RSC request for those segments, no re-render on the client.CartCounterkeepsitemsbecause its component instance never left. - Below the shared layout, the
hatspage segment is rendered. It's static and was prefetched when its link entered the viewport, so the observed click made no network request at all. ShirtsPageisn't destroyed: with Cache Components it's put in a hidden<Activity>, which is why the observed DOM still hadShirts (hidden). Without Cache Components it would be unmounted.- This is the whole point of nested layouts: shared UI is rendered once, keeps its state, and navigation only pays for the part that changed.