Layouts, templates & navigation
The frame that stays while the content changes
A layout wraps everything below it and survives navigation – state and all. That is exactly what makes switching pages feel instant.
Header, navigation, sidebar: things that look the same across many pages. In Next.js you write them once in a layout.tsxA frame around everything below it – header, navigation, sidebar. It stays put while you navigate and keeps its state.→ – and they stay untouched when the page changes.
// The root layout is mandatory. Only here do <html> and <body> live.
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
} Layouts nest. If app/blog/layout.tsx exists too, it wraps every blog page – and is itself wrapped by the root layout.
app/layout.tsx header + footer
app/blog/layout.tsx sidebar with categories
app/blog/page.tsx post list
app/blog/[slug]/page.tsx a single post
Moving from /blog to /blog/hello:
→ both layouts stay put
→ only the innermost part is swapped A layout is the picture frame, the page is the picture inside. Switching swaps the picture – the frame stays on the wall, along with everything attached to it.
Because the layout stays, it keeps its state: an expanded navigation stays open, the sidebar keeps its scroll position, a video inside keeps playing. With an ordinary page load all of that would be gone.
Layout or template?
layout.tsx stays put, keeps state
template.tsx rebuilt on every navigation
Reach for a template when …
• a fade-in animation should run every time
• a useEffect should fire on every page change
• state should deliberately be reset When in doubt: layout.tsxA frame around everything below it – header, navigation, sidebar. It stays put while you navigate and keeps its state.→. A template.tsxLike a layout, but rebuilt on every navigation. For things that should start over each time – a fade-in animation, say.→ is the exception and costs you the very advantage layouts have.
The active link
A navigation should show where you are. For that it needs the current path – which makes it a Client componentA component that runs in the browser and can therefore use state, hooks and clicks. Marked with `"use client"` at the top of the file.→.
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
const items = [
{ href: "/", label: "Home" },
{ href: "/blog", label: "Blog" },
{ href: "/contact", label: "Contact" },
];
export function Navigation() {
const path = usePathname();
return (
<nav>
{items.map((i) => {
const active = i.href === "/" ? path === "/" : path.startsWith(i.href);
return (
<Link key={i.href} href={i.href} aria-current={active ? "page" : undefined}>
{i.label}
</Link>
);
})}
</nav>
);
} A common reflex is to put "use client" in layout.tsx. That moves the entire frame into the browser. The right move: the layout stays a server component and pulls in the small navigation component.
What a layout cannot do
- +
Shared styling and structure
- +
Loading data that applies to the whole area (menu, current user)
- +
Setting metadata for everything below
- −
Reacting to the current path – a layout does not know which page sits inside it
- −
Reading
searchParams– only the page gets those - −
Running something on every navigation – that is what templates are for
That is the whole point – but it can surprise you if a layout loads data that should differ per subpage. Such data belongs in the page, not in the frame.
▸ In more depth: why navigation is so fast in the App Router optional
Clicking a next/linkAn internal link that prefetches its target in the background as soon as the link becomes visible.→ does not reload the page. Next.js fetches only the part that changes – and thanks to the layout structure it knows exactly which part that is.
Link becomes visible → target is prefetched in the background
Click → only the innermost part is requested
Response → layouts stay, content is replaced
Back button → page comes straight from the router cache The Router cacheThe store in the browser: navigating back shows the page instantly from memory.→ in the browser keeps recently visited pages around for a short while. Navigating back therefore shows something instantly – occasionally something stale. After a change you clear it with router.refresh() or, more precisely, with revalidatePath / revalidateTagTargeted invalidation: after saving, refetch exactly the pages or data that are affected.→.
Does it stick?
4 questions on this lesson. Wrong answers show up in your stats.
Now write it yourself
The navigation should show where you are. In Next.js the path would come from usePathname(); here it arrives as a prop so the logic is testable.
- □The matching item gets
aria-current="page" - □
/blog/my-postmarks theBlogitem - □The home item
/is active only at exactly/, not everywhere
const ITEMS = [ { href: "/", label: "Home" }, { href: "/blog", label: "Blog" }, { href: "/contact", label: "Contact" }, ]; // In Next.js: "use client" + const path = usePathname(); export function Navigation({ path }: { path: string }) { return ( <nav> {ITEMS.map((i) => { // TODO: work out whether this item is active const active = false; return ( <a key={i.href} href={i.href} aria-current={active ? "page" : undefined} style={{ marginRight: 12, fontWeight: active ? 700 : 400 }} > {i.label} </a> ); })} </nav> ); } export default function App() { return ( <div style={{ fontFamily: "system-ui", padding: 16 }}> <Navigation path="/blog/my-post" /> </div> ); }