A Next.js App Router app adds a shared header in app/layout.tsx that awaits cookies() to greet the signed-in user. After that change, every route in the build output is listed as Dynamic, including the marketing pages. Explain why one call in the layout affects unrelated pages, and how you would get those pages prerendered again.
answer
- the unit is the route, not the component
- the root layout is in every route
- shared layout, shared constraint
- split the layouts or move it clientside
- the call may be three imports deep
basics
~20 sNext.js decides static versus dynamic per route, and a route is the composition of the root layout plus every nested layout and the page. The root layout is part of every route, so its cookies() call opts them all out of prerendering.
solid answer
~50 sRendering mode is a property of the whole route, not of a component. A route is rendered as one unit made of the root layout, any nested layouts and the page, so anything the root layout does applies to every route in the app. Awaiting `cookies()` there means no route can be produced before a request exists, and the build marks them all Dynamic. To get the marketing pages back, remove the request-time read from the shared path: put the personalized greeting in a Client Component that fetches the session after hydration, or give the marketing section its own layout — typically via a route group — that never reads cookies. Partial Prerendering is the framework-level answer to keeping one dynamic hole in an otherwise static page, but it changes the rendering model rather than the layout's reach.
go deeper
Be able to say that layouts are part of every route below them, so a request-time read in the root layout applies to the whole app rather than to that one component.
Explain that the route — root layout plus nested layouts plus page — is the unit Next classifies, and give two concrete fixes: move the read to the client, or split the layout so only the personalized section carries it.
Show how you would find the call when it is buried in a shared dependency, and weigh the flash-of-signed-out cost of a client-side fetch against duplicating a layout for the static section.
Own the rule that the root layout is a shared constraint on every route, and put a check on it — build-output review, or a guard on the routes that must stay static — rather than trusting code review to catch the next one.
## A route is rendered as one unit The App Router composes a route from files down a path: the root `app/layout.tsx`, each nested `layout.tsx` along the way, and finally the `page.tsx`. When Next.js asks "can this be prerendered?" it asks it of that whole composition, not of individual components. The answer is no as soon as any part of it needs the incoming request. The root layout sits in every single route in the app. Put `await cookies()` in it and you have declared that *no* route in the app can be produced before a request arrives — which is exactly what the build output is telling you. It is not a bug and not a propagation quirk; it is the definition of the unit being classified. ```tsx // app/layout.tsx — this one line reclassifies the entire app import { cookies } from 'next/headers' export default async function RootLayout({ children }: { children: React.ReactNode }) { const name = (await cookies()).get('name')?.value return ( <html lang="en"> <body> <header>{name ? `Hi, ${name}` : 'Sign in'}</header> {children} </body> </html> ) } ``` ## Why this specific mistake is so common The personalized header is the archetype: it is genuinely global UI, so it genuinely belongs in a shared layout, and the data it wants is genuinely request-scoped. Every app hits this. The same shape appears with a locale read from a header, a feature-flag SDK that inspects cookies, an A/B assignment, or an analytics helper that calls `headers()` three imports deep inside a shared package — the call does not have to be visible in the layout file to count. The symptom is rarely a broken page. It is a cost and latency change: pages that used to be served from prerendered output now execute server work on every request, so origin traffic, cold starts and TTFB all move at once, usually days after the offending pull request merged. ## Getting the static pages back The fix is always the same in shape: get the request-time read out of the shared path. **Move it to the client.** Render a neutral header on the server and have a small Client Component fetch the session after hydration, showing a skeleton or the signed-out state first. The route becomes prerenderable again. You pay for it with a visible flash of the signed-out header and one extra client request — acceptable for a greeting, not for anything that changes page structure or leaks a wrong state. **Split the layouts.** If only part of the app is personalized, stop sharing a layout between the parts. Give the marketing pages their own layout that reads nothing request-scoped — a route group is the usual mechanism, since it lets you change the layout without changing the URLs. The application section keeps its dynamic layout; the marketing section goes back to being prerendered. **Read it somewhere other than render.** Some request-scoped decisions do not belong in the render at all. Middleware can redirect or rewrite based on a cookie without any page touching `cookies()`, and a route handler can serve the session to the client. These move the read out of the rendering path entirely. **Use Partial Prerendering.** The framework-level answer to "a static page with one dynamic hole" is PPR, which prerenders the shell and streams the dynamic part per request. It is worth naming in an interview as the direction the framework is going, but it is a change in rendering model, not a fix for the layout's reach. ## What to say about the tradeoff The substantive answer is not "move it to the client" — it is that a shared layout is a shared *constraint*. Anything you put in the root layout you are putting in every route, and request-time reads are the most expensive thing you can put there. Reviewing changes to the root layout with that in mind, and confirming the build output after they land, is what prevents the next occurrence.
- Does the same thing happen if a nested layout, rather than the root layout, reads cookies()?Yes, but the blast radius is smaller. A nested layout is part of every route beneath it, so those routes go dynamic while routes outside that subtree are unaffected. That containment is exactly why splitting layouts — usually with a route group — is the standard fix.
- The header component does not call cookies() directly; a shared analytics package it imports does. Does that still count?Yes. Next classifies the route by what actually executes during the render, not by what the route's own files look like. A call inside any imported module counts the same, which is why these regressions are hard to spot in review and easy to spot in the build output.
- If you move the greeting into a Client Component, what have you actually traded away?Correct output at first paint. The server now sends the signed-out header, and the personalized one appears after hydration and a client request — a visible flash. That is fine for a greeting, poor for anything that changes layout or would show a wrong entitlement, and it adds a request the static page did not make.
A shared layout is an ingredient in every dish on the menu. Make it require something that only arrives with the customer's order, and nothing can be cooked ahead of time.
saying these in an interview costs you the question
- Thinks only the layout component itself renders per request
- Says the pages must be individually opted out with a config export
- Blames the pages instead of the shared layout in the route
- Assumes wrapping the header in Suspense restores prerendering by itself
- Believes 'use client' on the layout would fix the rendering mode