Concept
The beginner framing: the App Router is Next.js's routing system where the folder structure inside app/ directly determines your application's URL structure, no separate router configuration file to maintain.
The precise mental model: two special files do almost all the work. A page.tsx file makes a route segment publicly reachable and renders its unique UI. A layout.tsx file wraps shared UI around one or more pages, and, critically, layouts preserve state, remain interactive, and never re-render on navigation between the pages they wrap.
app/
layout.tsx ← root layout, REQUIRED, wraps everything
page.tsx ← the "/" route
blog/
layout.tsx ← wraps every route under /blog
page.tsx ← the "/blog" route
[slug]/
page.tsx ← the "/blog/:slug" route// app/layout.tsx, the root layout is required and must contain html/body
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
// app/blog/page.tsx, a page, rendered where {children} appears above
export default async function BlogPage() {
const posts = await getPosts(); // Server Component, can await data directly
return <ul>{posts.map((p) => <li key={p.id}>{
Layouts nest exactly the way folders nest: navigating from /blog to /blog/hello-world re-renders only app/blog/[slug]/page.tsx, the root layout and the blog layout both stay mounted, untouched.
Server Components by default
Every page.tsx and layout.tsx is a React Server Component unless you explicitly opt out. This means, by default, every route in your app can await data directly in its component body, never ships its own JavaScript to the browser, and can safely touch secrets and database credentials, the same model covered in Server Components. Interactivity (state, event handlers, browser APIs) requires explicitly marking a component "use client", covered in depth in Server/Client Component Composition.
The rest of the special-file vocabulary (previewed here, each covered in depth later)
| File | Purpose | Covered in |
|---|---|---|
loading.tsx | Automatic Suspense fallback for a segment | Loading & Error UI |
error.tsx | Automatic error boundary for a segment | Loading & Error UI |
not-found.tsx | 404 UI for a segment |
Try It
Predict what happens before checking the solution.
app/
layout.tsx (Root Layout)
dashboard/
layout.tsx (Dashboard Layout)
page.tsx ("/dashboard")
settings/
page.tsx ("/dashboard/settings")A user is on /dashboard (with some client-side state inside Dashboard Layout, like an open sidebar) and clicks a link to /dashboard/settings. Does the sidebar's open/closed state survive?
Solution
Yes. Only app/dashboard/page.tsx is being swapped out for app/dashboard/settings/page.tsx, both Root Layout and Dashboard Layout wrap both routes and are never unmounted during this navigation. Any client-side state living in Dashboard Layout (or anything above it) survives completely untouched, exactly as the "layouts preserve state, remain interactive" guarantee describes.
Implement It Yourself
Build a minimal version of the App Router's core matching logic, folders to URL segments, with page/layout file lookup:
function buildRouteTree(folderStructure) {
// folderStructure: { name, page?: Component, layout?: Component, children: [] }
function render(node, urlSegments, childOutput) {
const content = node.page ? node.page() : childOutput;
return node.layout ? node.layout({ children: content }) : content;
}
function matchAndRender(node, segments) {
if (segments.length === 0) {
return render(node, segments, null);
}
const [next,
This mirrors the real mechanism closely: matching is a recursive walk down the folder tree by URL segment, and rendering wraps outward-in, the deepest matched page renders first, then each ancestor's layout wraps that result moving back up to the root.
Under the Hood
Every layout/page being a Server Component by default is a direct application of Server Components's core distinction, the App Router's file conventions are really just a routing-and-composition layer built on top of that React primitive. And a layout "not re-rendering" on navigation is the same nested-component-tree idea from Components: Layout renders {children} without knowing or caring what's inside, swapping the page underneath it is exactly like swapping a prop, not touching Layout itself at all.
Common Mistakes
1. Forgetting the root layout must contain <html> and <body>
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <div>{children}</div>; // ❌ missing html/body, build error
}Every other layout in the tree is a fragment of shared UI; the root layout specifically must own the actual document shell, since it's the only layout guaranteed to wrap every single route in the app.
2. Adding a page.tsx where only a layout.tsx was intended
A folder with a layout.tsx but no page.tsx is not itself a reachable route, it's purely a wrapper for its children. Forgetting this can lead to confusion about why a folder "doesn't have a page" even though it visually groups related routes.
3. Expecting a layout to receive params the same way a page does without asking for them
Layouts can receive params for their own segment (typed via LayoutProps<'/route'>), but a common mistake is expecting a layout higher up the tree to automatically know about a child segment's dynamic params, it doesn't, unless that segment is the layout's own.
Best Practices
- Keep the root layout minimal, the document shell, global providers, and anything truly global. Push feature-specific UI down into nested layouts.
- Default to Server Components for every route, opting into
"use client"only for the specific interactive pieces (see Server/Client Component Composition). - Use nested layouts to share UI, not by repeating markup across every
page.tsx, a shared sidebar or nav belongs in a layout, not copy-pasted. - Reach for the
PageProps/LayoutPropsglobal helper types (auto-generated bynext dev/next build) to typeparams/ correctly instead of hand-writing them.
Performance Tips
- A layout that never re-renders on navigation between its child pages means its own data-fetching (if any) only happens once per visit to that subtree, colocate genuinely page-specific data fetching in the page itself, not a shared layout, to avoid needlessly re-fetching layout data on every request.
- Because most routes are Server Components by default, the App Router's baseline is already "ship less JavaScript", the discipline covered in Server/Client Component Composition is about not giving that advantage away accidentally.
