Layouts and route groups
The Next.js App Router conventions for shared UI, layout.tsx, nested layouts, template.tsx, and route groups, that give your pages a persistent shell.
In the last lesson you learned that the file system under src/app/ is the route table: a folder is a URL segment, and a page.tsx is the leaf the framework renders at that URL. That gets you a /dashboard and a /dashboard/invoices. But a real dashboard has a sidebar, a top bar, maybe a notification bell, the chrome that has to appear on every dashboard page. The /sign-in page needs none of that; it wants a bare centered card and nothing else.
The moment you have more than one page, two questions follow. First, how do you share that sidebar across the whole dashboard without rebuilding it on every navigation? With nothing but React, you’d reach for a <Shell> component and import it into each page.tsx. That works, but barely, and it’s wrong on two counts. The shell remounts every time you change pages, so a scrolled or collapsed sidebar snaps back to its starting state on each navigation. You’ve also hand-copied the same wrapper into every file. Second, how do you give /sign-in and /dashboard completely different shells without leaking an /auth/ or /app/ prefix into their URLs?
This lesson installs the two file-system conventions that answer those questions: layout.tsx for the shared shell, and route groups for organizing routes under different shells with no URL impact. They extend the same contract from last lesson, where the file system describes the route table and the framework reads it. Everything you build here lands on one structure, a SaaS app split into an (auth) shell and an (app) shell, which is the shape you’ll carry into every multi-page surface for the rest of the course.
layout.tsx, the shell every nested route renders inside
Section titled “layout.tsx, the shell every nested route renders inside”A layout.tsx is the framework’s answer to “shared shell.” Drop one into a folder, and every page.tsx at or below that folder renders inside it. Put your sidebar in src/app/dashboard/layout.tsx and it wraps /dashboard, /dashboard/invoices, and /dashboard/settings, every page in that subtree, written once.
The shape is small, but two facts about a layout trip up nearly everyone the first time. To see both, we’ll walk the root layout, the one file every Next.js app has: src/app/layout.tsx, which wraps your entire application.
import './globals.css';
import type { ReactNode } from 'react';
export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body> <a href="#main-content" className="sr-only focus:not-sr-only"> Skip to content </a> <main id="main-content" tabIndex={-1}> {children} </main> </body> </html> );}The leaf-file rule from last lesson holds for layouts too: a framework file is the one place a default export is sanctioned, and the framework finds the layout by its position in the file tree, not by the function’s name.
There is no 'use client' directive, so like page.tsx, a layout is a Server Component by default. We rely on that here; the full Server/Client story is the next chapter.
import './globals.css';
import type { ReactNode } from 'react';
export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body> <a href="#main-content" className="sr-only focus:not-sr-only"> Skip to content </a> <main id="main-content" tabIndex={-1}> {children} </main> </body> </html> );}A layout takes a single prop, children, and the framework fills it; you never pass it yourself.
Whatever route renders inside this layout arrives here as children. The instinct is to hand children in the way you would to any other component, but here you receive them instead.
import './globals.css';
import type { ReactNode } from 'react';
export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body> <a href="#main-content" className="sr-only focus:not-sr-only"> Skip to content </a> <main id="main-content" tabIndex={-1}> {children} </main> </body> </html> );}Only the root layout returns <html> and <body>, because it owns the document shell.
Every nested layout you write later returns a fragment or a plain <div> that lands inside this <body>. Forgetting that and adding a second <html> in a nested layout is a common early mistake.
import './globals.css';
import type { ReactNode } from 'react';
export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body> <a href="#main-content" className="sr-only focus:not-sr-only"> Skip to content </a> <main id="main-content" tabIndex={-1}> {children} </main> </body> </html> );}This is where the shared chrome sits relative to the page. Anything you place around {children} appears on every page this layout wraps. Here that’s the skip link the accessibility baseline asks every layout to carry, paired with the <main id="main-content"> it jumps to.
The page itself slots in exactly where {children} is written.
The rest of the <html>/<body> business, meaning the lang attribute, the page title, fonts, and the metadata export, is owned by a later chapter on metadata and the document head. Treat it here as structure, not as something to configure: the root layout is where the document shell lives, and that’s all you need from it today.
That second step is worth restating, because the instinct to pass children yourself is strong. You don’t construct the children; the framework hands them to you. The page that resolves at a URL becomes the children of the layout above it.
The page that resolves at /dashboard is the children the framework passes into the layout above it. You wire none of this by hand; matching the folder positions is the whole wiring.
A quick word on the types you just saw. ReactNode is the right type for children precisely because it’s the widest one: a page might render an element, a string, a list, anything. And a Server Component is the default for both pages and layouts; you only opt a file out with a directive, which the next chapter covers.
Layouts compose down the tree
Section titled “Layouts compose down the tree”A layout doesn’t replace the layout above it; it nests inside it. The framework walks from the root down to the page you requested, and at every folder that has a layout.tsx, it wraps what it’s built so far. Root layout, then the dashboard layout, then the page: each one wraps the last, like a set of nesting dolls where the page is the smallest doll in the center.
Scrub through the sequence below to watch a /dashboard request get assembled from the inside out.
The spatial nesting in that diagram maps one-to-one to the folder nesting on disk. Three files, three rings:
Directorysrc/
Directoryapp/
- layout.tsx the root shell —
<html>/<body> Directorydashboard/
- layout.tsx the dashboard shell — sidebar
- page.tsx the leaf
- layout.tsx the root shell —
Written as code, the framework composes those three files into this:
<RootLayout> <DashboardLayout> <DashboardPage /> </DashboardLayout></RootLayout>RootLayout receives DashboardLayout as its children, and DashboardLayout receives DashboardPage as its children. That’s the whole composition rule. Once you see the tree nested like this, the property the next section turns on follows almost for free.
What re-renders on navigation: the layout/page boundary
Section titled “What re-renders on navigation: the layout/page boundary”Here is the idea the rest of the lesson builds on:
Layouts stay mounted across navigations within their subtree. Only the page swaps.
Navigate from /dashboard to /dashboard/invoices, and the framework does not rebuild the tree. It keeps RootLayout and DashboardLayout exactly as they are, mounted and untouched, and swaps the innermost doll, the page, for the new one. The shells stay put; only the center changes.
If you arrived here from React with the assumption that navigation re-renders everything, this is the moment to update it. The consequences are large:
- State held in a Client Component inside a layout survives navigation within that subtree. A sidebar’s collapsed-or-expanded toggle, its scroll position, a podcast player tucked in the corner that keeps playing as you move between pages: all of it persists, because the layout never unmounted.
- State held in the page does not survive. The page unmounts and a fresh one mounts in its place, so its local state resets to its initial value.
From those two facts comes a reflex you’ll apply for the rest of the course: persistent UI lives in the layout; transient, per-page UI lives in the page. Anything that should outlive a navigation goes up into a layout. Anything that should reset when the route changes stays in the page.
The figure below simulates that render boundary. The tree is a dashboard layout, holding the sidebar’s open/closed state, wrapping a page. Click each trigger and watch which boxes light up.
On the layout.tsx variant, the navigation lights up only the page, while both layouts hold steady. That single flash is why the sidebar’s scroll survives and why the corner player keeps playing, and it’s the reason layouts exist in the first place. Flip to the React-only variant and the same navigation remounts the entire imported shell, so root, dashboard, and page all flash and the sidebar state is gone. That is the cost the intro warned about, now made visible.
Notice the second trigger on the layout.tsx variant, too. Toggling the sidebar re-renders DashboardLayout, which owns that state, and the page beneath it, but a different navigation later still won’t touch that state. The layout keeps its state between page changes; the page keeps nothing.
The same property that makes layouts efficient has a consequence for data fetching, worth catching now. Since a layout doesn’t re-run on a page-only navigation, a layout that fetches data fetches it once and holds it. That’s ideal for data the whole subtree shares, but wrong for data a single deep page needs. Put a fetch for one page’s data up in a high layout and you block that entire subtree from rendering until the fetch resolves, while a navigation to a sibling page re-fetches nothing. The rule is simplest stated positively: fetch at the level that owns the data. Page-specific data is fetched in the page. How a layout streams while that data loads, using loading.tsx and Suspense at the layout boundary, belongs to a later chapter; here you only need the placement rule.
Test the boundary on yourself. Sort each piece of state by what happens when you navigate from /dashboard to /dashboard/settings.
Sort each item by what happens when the user navigates from /dashboard to /dashboard/settings. Drag each item into the bucket it belongs to, then press Check.
DashboardLayout)DashboardLayout)<html lang="en"> attribute (in the root layout)/dashboard page/dashboard page’s tableNested layouts
Section titled “Nested layouts”Two levels is the common case, but nothing stops you from going deeper. The composition rule you just learned doesn’t change; it just stacks one more doll. Say the invoices section of the dashboard wants its own tab bar (Overview, Drafts, Paid) above every invoice screen. That’s a third layout, dashboard/invoices/layout.tsx, nested inside the dashboard layout, which is itself nested inside the root.
Directorysrc/
Directoryapp/
- layout.tsx root — owns
<html>/<body> Directorydashboard/
- layout.tsx app shell — sidebar + header
Directoryinvoices/
- layout.tsx invoices shell — section tab bar
- page.tsx the leaf
- layout.tsx root — owns
Each ring owns one job: the root owns the document shell, the dashboard layout owns the app chrome, the invoices layout owns the section’s tab bar, and the page owns the invoice itself. Every nested layout returns a fragment or a wrapper; only that single root at the top carries <html> and <body>. The composition is exactly what you already saw, one doll deeper.
template.tsx, when remount is the call
Section titled “template.tsx, when remount is the call”Everything so far has been the default, and the default is what you want roughly all the time: a layout that persists. But persistence is occasionally the wrong behavior, and Next.js gives you a second file for exactly those cases: template.tsx.
A template.tsx has the same shape as a layout.tsx: a default export, a children prop, a Server Component by default. The one difference is the behavior you just learned to rely on. The framework gives a template a fresh key on every navigation into its segment, so instead of persisting, it remounts every time. A new instance mounts, its DOM subtree is recreated, any Client Component state inside it resets to its initial value, and its effects fire again.
The two files sit side by side below: near-identical code, opposite behavior.
export default function DashboardLayout({ children }: { children: ReactNode }) { return <section className="p-6">{children}</section>;}Persists, the default. Mounts once and stays mounted across navigations in this subtree; Client Component state inside it survives. Reach for this unless you have a specific reason not to.
export default function DashboardTemplate({ children }: { children: ReactNode }) { return <section className="p-6">{children}</section>;}Remounts per navigation, the exception. A fresh instance every time you navigate into the segment; state resets, effects re-fire. Identical code, opposite behavior: the filename is the whole switch.
So when is remount actually the call? The list is short. Treat layout.tsx as the default and reach for template.tsx only on one of these triggers:
- A child’s state should reset on every navigation. A “new record” form that must come up blank each time you open a different record, rather than carrying the last record’s half-typed values.
- A
useEffectmust re-synchronize on every navigation, such as a per-page analytics ping that should fire on each view, or an enter animation that should replay every time you arrive. - You want to change how a Suspense fallback behaves. This is the non-obvious one: a Suspense boundary placed in a layout shows its fallback only on the first load, because the layout persists; the same boundary in a template shows it on every navigation, because the template remounts. The streaming details belong to a later chapter, but the trigger is worth knowing: if you want the loading state to reappear on each navigation, a template is how.
A few precise facts keep the model exact rather than approximate. A template renders between the layout and the page, as <Layout><Template key={...}>{children}</Template></Layout>, so within one segment it wraps the page, not the layout. The key is per-segment: navigating around inside a deeper segment won’t remount a template higher up, and a change to the search params doesn’t remount it either. The two files can also coexist in one folder, so a segment can have both a layout.tsx and a template.tsx, persisting the shell while remounting the part inside the template.
The verb to keep is remount : the old instance is thrown away and a new one is built. That’s the entire difference between the two files.
Route groups: organize siblings without touching the URL
Section titled “Route groups: organize siblings without touching the URL”Now to the second question from the start of the lesson. The dashboard has its shell. But /sign-in wants a different shell, a bare centered card with no sidebar, and you don’t want a /auth/ segment showing up in the URL just because you grouped the auth pages together in a folder. You need a folder that organizes routes but contributes nothing to the URL.
That’s a route group. A folder whose name is wrapped in parentheses is invisible to the URL. Name a folder (app) and the segment vanishes from every path inside it: src/app/(app)/dashboard/page.tsx serves /dashboard, not /app/dashboard. The parentheses signal to the router that this folder is for organization and should be skipped when building the URL.
This sounds a lot like the private _folder from last lesson, and the two are easy to confuse because both “disappear.” The distinction is worth pinning down, because they disappear in opposite ways. A _folder hides from routing entirely: nothing inside it is routable, it produces no URL at all, and that’s where you stash colocated components and helpers. A (folder) is fully routable, since its pages absolutely produce URLs; it just contributes zero segments to those URLs. One is “not a route”; the other is “a route, minus its own segment.”
Route groups earn their place three ways, in order of how often they matter:
- Give sibling subtrees different shells, with no URL prefix. This is the one driving this whole lesson: the
(auth)/(app)split. Each group gets its ownlayout.tsx, and neither group’s name shows up in a single URL. - Opt a cluster of routes into a shared layout while leaving others out. Wrap the routes that should share a shell in a group with a layout; leave the rest outside it.
- Organize routes by domain or team, purely for the humans reading the repo, with no effect on the URL whatsoever.
Here is the structure the whole lesson has been building toward: one app, two shells, no URL prefix on either.
Directorysrc/
Directoryapp/
- layout.tsx the one root —
<html>/<body>, app-wide providers Directory(auth)/ route group — no URL segment
- layout.tsx centered card, no chrome
Directorysign-in/
- page.tsx →
/sign-in
- page.tsx →
Directorysign-up/
- page.tsx →
/sign-up
- page.tsx →
Directory(app)/ route group — no URL segment
- layout.tsx sidebar + header
Directory
_components/- sidebar.tsx co-located with its layout (Principle #1)
Directorydashboard/
- page.tsx →
/dashboard
- page.tsx →
- layout.tsx the one root —
Read down the tree and the rule is visible on every row: (auth) and (app) contribute nothing, so (app)/dashboard/page.tsx resolves to plain /dashboard and (auth)/sign-in/page.tsx to plain /sign-in. The two groups carry two entirely different layouts, and the user never sees a hint of either group name in the address bar.
Notice sidebar.tsx sitting in (app)/_components/. That’s Architectural Principle #1, co-locate by feature, applied to a layout: the sidebar is used only by the (app) shell, so it lives right beside that shell in a private folder, not in a top-level components/. It only graduates to a shared components/ the day a second group needs it. Layout-specific code lives with its layout.
Fill in the URLs below to lock the rule in. Each (group) contributes nothing; each real segment contributes its name.
Fill in the URL each file resolves to. Each (group) contributes nothing; each real folder contributes its name. Pick the right option from each dropdown, then press Check.
src/app/(app)/dashboard/page.tsx → ___src/app/(auth)/sign-up/page.tsx → ___src/app/(marketing)/pricing/page.tsx → ___One root, many shells, and the collision that crashes the build
Section titled “One root, many shells, and the collision that crashes the build”The structure above made a quiet decision worth saying out loud, because it’s the default an experienced engineer reaches for in a SaaS app: keep one root. There’s a single src/app/layout.tsx that owns <html>, <body>, and app-wide providers, and each group gets its own layout.tsx for its distinct chrome. Those group layouts are just nested layouts that happen to sit one level under the root; they return a fragment or a wrapper, never <html>/<body>. One document shell at the top, many shells branching beneath it.
There is a way to give each group a truly independent <html>/<body>, with a different lang and different top-level providers, the sort of split you’d want between a marketing site and an app that share no chrome at all. You drop the top-level src/app/layout.tsx entirely and put a layout.tsx with its own <html>/<body> inside each group, making each group its own root. You will almost never do this in a SaaS app, and recognizing it when you see it is enough. Two real costs explain why one root is the default. Navigating between two roots triggers a full page reload, because the browser document is genuinely replaced, so you lose the instant client-side transition that makes the rest of the app feel seamless. And with no top-level layout, the home route / has to live inside one of the groups, since there’s no longer a shared root for it to attach to.
Choosing the right tool
Section titled “Choosing the right tool”You’ve now met four moves: a single layout.tsx, a nested layout.tsx, a template.tsx, and a route group. Most of the time the answer is the first one plus a route group; the rest are conditionals you reach for on a specific, named trigger. Walk the decision below the way you’d actually ask it: start from what you’re trying to do, and let the question narrow to a file.
One layout.tsx at the top of the subtree.
It mounts once and persists across navigations, so put the sidebar, header, and any state that should survive a page change here.
This is the default choice.
Wrap each cluster in a (group) and give each group its own layout.tsx.
The groups contribute no URL segment, so each subtree gets its own shell with no /auth/ or /app/ prefix.
This is the (auth)/(app) split.
The only file that remounts on every navigation into its segment, so state resets and effects re-fire.
Earn it on a real trigger: an input that must reset per record, a per-view analytics ping, or a Suspense fallback that should reappear on every navigation.
Otherwise stay on layout.tsx.
Add a second (or third) layout.tsx deeper in the tree.
It composes inside the layout above it, one more doll, and returns a fragment, never <html>/<body>.
Use it for a section-level shell like an invoices tab bar.
Wrap the routes in a (group) for organization and stop there.
No layout, no URL change, purely a way to group routes by domain or team in the repo.
Run that walk against your own app and the four conventions collapse into a single habit: reach for layout.tsx plus a route group by default, and step off the default only when a trigger tells you to.
External resources
Section titled “External resources”The Next.js documentation covers these conventions with the framework’s own framing and a few edge cases this lesson set aside.
The getting-started guide to page.tsx, layout.tsx, and how they nest.
The file-convention reference for (folder) groups, including multiple-root-layout rules.
The reference for the remount-on-navigation file, with the per-segment key walkthrough this lesson summarized.
Why a navigation keeps shared layouts mounted: the client-side transition and partial rendering behind the whole render-boundary section.