A layout wraps everything below it
app/layout.tsx wraps every route. app/doors/layout.tsx wraps /doors and everything under it. They nest, outermost first, and the page ends up at the bottom.
export default function DoorsLayout({ children }) {
return (
<section className="doors">
<DoorsNav />
{children} {/* the page, or a deeper layout */}
</section>
);
}
They do not re-render on navigation
Moving from /doors/12 to /doors/13 renders the page again and leaves the layout alone. Its state survives, its scroll position survives, and a video in it keeps playing.
That is the whole point, and it is also the surprise: a layout cannot read the current route’s parameters, because it is not re-rendered when they change.
export default function Layout({ children, params }) {
// params of ITS OWN segment only — app/doors/[id]/layout.tsx gets { id }
// there is no searchParams here, and no pathname
}
// need the current path in the shell? a small client component:
"use client";
const pathname = usePathname();
Template: the opposite
A template is a layout with a changing key. Use one when something must start fresh on every navigation — and only then, because remounting is work.
export default function Template({ children }) {
return <div className="fade-in">{children}</div>;
}
| layout.tsx | template.tsx | |
|---|---|---|
| On navigation | Stays mounted | Remounted |
| State inside it | Survives | Reset |
| Effects inside it | Do not re-run | Run again |
| Use for | Navigation, shells, providers | Enter animations, per-page logging, resetting a form |
Groups give different shells
Neither group appears in the URL. This is the standard way to have a marketing site and an application in one project without the paths growing a prefix nobody wanted.
app/(marketing)/layout.tsx ← a wide public shell
app/(marketing)/page.tsx → /
app/(marketing)/about/page.tsx → /about
app/(app)/layout.tsx ← a sidebar, and an auth check
app/(app)/doors/page.tsx → /doors
Metadata belongs to layouts too
// app/layout.tsx
export const metadata = {
title: { default: "Doors", template: "%s · Doors" },
};
// app/doors/[id]/page.tsx
export const metadata = { title: "Painted" }; // → "Painted · Doors"
Where providers go
// app/providers.tsx
"use client";
export function Providers({ children }) {
return <ThemeProvider><QueryClientProvider …>{children}</QueryClientProvider></ThemeProvider>;
}
// app/layout.tsx — still a server component
export default function RootLayout({ children }) {
return <html lang="en"><body><Providers>{children}</Providers></body></html>;
}
The children above are still server components. Wrapping the tree in a provider does not make everything below it a client component — only the provider itself.
What tends to go wrong
- Putting
"use client"on the root layout, which drags the whole application into the browser bundle. - Expecting a layout to re-run when the route changes — it does not. Use a client component with
usePathname, or a template. - Fetching the same data in a layout and in its page. Both run, and
fetchdeduplicates identical requests — but a database call is made twice.