
Next.js layout.js
layout.js is the outermost wrapper in a route segment's component hierarchy — it sits above template.js, error.js, loading.js, not-found.js, and page.js, and it's what makes shared UI (navigation, sidebars, footers) something you write once instead of duplicating into every page. But the interesting part of this API reference isn't the basic "wrap children in a section" example every tutorial leads with — it's the specific, deliberate restrictions the framework places on what a layout can and can't do, and why those restrictions exist.
The Minimal Shape
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return <section>{children}</section>;
}
A layout wraps whatever the matching route resolves to for children. It's the segment-scoped version of shared UI — this one only wraps things under /dashboard, not the whole app.
The Root Layout
The app directory must include exactly one applicable root layout for any given URL — the top-most layout that has no layout.js above it. Typically that's app/layout.js:
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
The root layout carries a hard requirement no other layout does: it must define <html> and <body> tags. This isn't a stylistic default you can skip — Next.js is relying on the root layout to be the single place where the actual document shell gets constructed.
A related, easy-to-violate rule: don't manually add <head> tags like <title> or <meta> to root layouts. The Metadata API exists specifically so you don't have to hand-manage <head> de-duplication and streaming yourself — reaching for raw <title> tags in a root layout sidesteps a system built to handle exactly that correctly.
Multiple Root Layouts
Here's a detail that surprises people coming from frameworks with a single, fixed document shell: any layout without a layout.js above it is, by definition, a root layout — and you can have more than one in the same app. Two common ways this shows up:
- Using route groups —
app/(shop)/layout.jsandapp/(marketing)/layout.jseach become independent root layouts for their respective sections, letting a marketing site and a shop app share one codebase with completely different document shells. - Omitting
app/layout.jsentirely and letting subdirectory layouts —app/dashboard/layout.js,app/blog/layout.js— each become the root layout for their own tree.
The behavioral consequence worth knowing before you adopt this pattern: navigating across two different root layouts forces a full page load, not a client-side transition. Next.js can't preserve client-side state across a boundary where the entire document structure (potentially different <html> attributes, different fonts, different global providers) might change — so it doesn't try, and correctly falls back to a hard navigation instead.
Root Layouts Under a Dynamic Segment
A root layout can itself live under a dynamic segment — the standard pattern for internationalized routing, app/[lang]/layout.js. Dynamic segments that appear before the root layout in the tree get a special name: root parameters. Because there's nothing above the root layout to thread a prop through, root parameters are readable from any Server Component directly via next/root-params, rather than requiring you to manually pass params down through every intermediate layout.
Props Reference
children (required)
Every layout must accept and render children. During rendering, this gets populated with whatever the layout is wrapping — most often a nested layout or a page, but it can also be other special files like loading.js or error.js when those are what's actively rendering for that navigation.
params (optional)
A promise resolving to the dynamic route parameters accumulated from the root segment down to this layout:
export default async function Layout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ team: string }>;
}) {
const { team } = await params;
}
| Example Route | URL | params |
|---|---|---|
app/dashboard/[team]/layout.js | /dashboard/1 | Promise<{ team: '1' }> |
app/shop/[tag]/[item]/layout.js | /shop/1/2 | Promise<{ tag: '1', item: '2' }> |
app/blog/[...slug]/layout.js | /blog/1/2 | Promise<{ slug: ['1', '2'] }> |
As with page.js and route.js, params is a promise — await it or use React's use(). Synchronous access still works for now as a backward-compatibility path from pre-15 versions, but treat it as deprecated, not as a supported long-term pattern.
The LayoutProps Helper
Rather than hand-typing the props shape yourself, LayoutProps<'/route'> is a globally-available, auto-generated helper that infers your correct params type and any named Parallel Routes slots directly from your actual directory structure:
export default function Layout(props: LayoutProps<"/dashboard">) {
return (
<section>
{props.children}
{/* If app/dashboard/@analytics exists, it appears as a typed slot: */}
{/* {props.analytics} */}
</section>
);
}
These types are generated during next dev, next build, or next typegen — and once generated, LayoutProps is globally available without an explicit import. If you're used to manually writing prop interfaces for every layout, this is a meaningfully faster and more accurate default, since it reads directly from your file structure instead of a type you maintain by hand and can let drift out of sync.
Caveats: What Layouts Deliberately Can't Do
This is the section that actually matters for building correct mental models, because every restriction here is a deliberate performance tradeoff, not an accidental limitation.
No Access to the Raw Request
Layouts are cached client-side across navigations and do not re-render between sibling pages under the same layout. That's the entire performance benefit of layouts — you don't pay to re-render the sidebar every time someone clicks between dashboard tabs. But it comes with a direct consequence: by restricting layouts from accessing the raw request, Next.js prevents potentially slow or expensive request-dependent logic from silently creeping into a component that's supposed to be reusable and cheap to keep around.
If you do need request data — cookies, headers — inside a layout, headers() and cookies() are still available as explicit Server Component APIs; the restriction isn't "layouts can never touch this data," it's "layouts don't get an implicit request object the way pages effectively do."
No searchParams
Because layouts don't re-render on navigation, giving them access to searchParams would mean that value going stale the moment a sibling page's query string changed — a layout instance sitting in the client cache with an outdated search param it has no mechanism to refresh. The fix is architectural: put anything that needs live query params in the page (searchParams is a page-only prop) or in a Client Component using useSearchParams, since Client Components genuinely do re-render on navigation and can track that value correctly.
No pathname
Same underlying reason, same fix: a layout can't reliably know the current pathname without going stale, so usePathname inside a Client Component is the supported path for anything — active-nav-link styling, breadcrumbs — that needs to react to exactly where the user currently is.
Interaction With loading.js
Because loading.js sits below layout.js in the hierarchy, it structurally cannot provide a fallback for uncached or runtime data access happening inside the layout itself — calling cookies(), headers(), or an uncached fetch directly in a layout. What actually happens next depends on whether Cache Components is enabled:
- Without Cache Components: navigation blocks entirely until the layout finishes rendering.
loading.jsnever gets a chance to show anything, because the layout above it hasn't resolved yet. - With Cache Components:
loading.jsbehaves as a regular<Suspense>boundary rather than a special prefetch marker. Any runtime data access in the layout must be explicitly wrapped in its own<Suspense>boundary, or Next.js flags it with a build-time error rather than letting it silently block navigation. The static shell streams immediately, with the uncached content swapping in once it resolves.
Either way, the practical fix is the same: wrap runtime data access in the layout in its own local <Suspense> boundary, or — often the cleaner option — move that data fetch out of the layout entirely and into page.js, where loading.js was always designed to provide a fallback:
import { Suspense } from "react";
import { NavSkeleton } from "./nav-skeleton";
import { DashboardNav } from "./dashboard-nav";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<Suspense fallback={<NavSkeleton />}>
<DashboardNav />
</Suspense>
<main>{children}</main>
</>
);
}
Layouts Can't Pass Data to children
There's no prop-drilling mechanism from a layout down into whatever page it's wrapping — a layout literally cannot hand data to its children. If a layout and its page both need the same data, the supported pattern is to fetch it in both places and let deduplication handle the redundancy: React's cache() function memoizes a function's result across a single render pass, and fetch() requests specifically are automatically deduplicated by Next.js regardless — so fetching "the same thing twice" in a layout and a page costs you nothing extra on the network, even though it looks redundant in the code.
No Access to Child Segments
A layout can't see which specific route segment is currently active beneath it — that information lives structurally below it in the tree, invisible from where the layout sits. useSelectedLayoutSegment and useSelectedLayoutSegments, used from a Client Component, are the supported way to read that — commonly for exactly the "highlight the active nav link" pattern:
"use client";
import Link from "next/link";
import { useSelectedLayoutSegment } from "next/navigation";
export default function NavLink({
slug,
children,
}: {
slug: string;
children: React.ReactNode;
}) {
const segment = useSelectedLayoutSegment();
const isActive = slug === segment;
return (
<Link
href={`/blog/${slug}`}
style={{ fontWeight: isActive ? "bold" : "normal" }}
>
{children}
</Link>
);
}
Key Takeaways
| Restriction | Reason | Supported alternative |
|---|---|---|
| No raw request access | Layouts are cached and don't re-render per-page | headers()/cookies() explicitly, where genuinely needed |
No searchParams | Would go stale since layouts don't re-render on navigation | Page's searchParams prop, or useSearchParams in a Client Component |
No pathname | Same staleness problem | usePathname in a Client Component |
loading.js can't cover layout-level runtime data | loading.js sits below layout.js in the hierarchy | Wrap the access in its own <Suspense>, or move it into page.js |
Can't pass data to children | No prop-drilling mechanism exists | Fetch again in both places — cache() and fetch deduplication make this free |
| No visibility into active child segment | Segment info lives below the layout | useSelectedLayoutSegment(s) from a Client Component |
Every one of these restrictions traces back to the same root cause: layouts are designed to be cached and reused across navigations, and every capability that's missing is missing specifically because granting it would force the layout to re-render more often than the framework wants it to. Once that tradeoff clicks, the restrictions stop feeling arbitrary — they're the direct, deliberate cost of the caching behavior that makes layouts worth using in the first place.


