GAP·MAP

Next.js App Router

[ middle depth ]

Server Components render by default, Client Components opt in

Every file under app/ is a Server Component unless its top line is 'use client'. Server Components run only on the server: they can be async and await a database or fetch, read secrets and cookies(), and they ship no JavaScript. They cannot use useState, useEffect, event handlers like onClick, browser globals such as window, or React Context. Any of those needs a Client Component, which still server-renders to HTML on the first load and then hydrates in the browser.

Wrong "'use client' makes a component render only in the browser." It still runs on the server for the first paint; the directive marks where interactivity is added, not where rendering happens.

What the use client boundary marks

The directive goes at the very top of a file, before imports, and declares a network boundary: every module that file imports joins the client bundle, and the directive is inherited down that import graph, so you never repeat it in child files. The rule runs one way. A Server Component can render a Client Component, but a Client Component cannot import a Server Component. The escape hatch is composition: a Client Component can receive a Server Component through children or props, since that arrives as already-rendered output rather than an import.

// page.tsx (a Server Component)
export default function Page() {
  // Cart renders on the server, then slots into the client Modal as output
  return <Modal><Cart /></Modal>
}

Keep the directive on the interactive leaf and pass server content in. Props crossing the boundary must be serializable, so an onClick function cannot travel from a Server to a Client Component.

Layouts persist, templates remount

A layout.tsx wraps a segment and its children, and across navigation between sibling routes it does not re-render: its DOM and any Client Component state inside it survive. Because it never re-runs per request, a layout cannot read searchParams or pathname without them going stale; read those in a Client child with useSearchParams() or usePathname(). A template.tsx instead gets a fresh key per segment, so it remounts on every navigation, resetting client state, re-running effects, and rebuilding its DOM. Default to a layout; reach for a template only when you want that reset, such as re-firing an entrance animation.

Route groups organize without changing the URL

Wrap a folder name in parentheses and it drops out of the URL: app/(marketing)/about/page.tsx still serves /about. Groups sort routes by team or feature, opt a subset of segments into a shared layout, or hold more than one root layout, all without adding a segment.

How loading and error files wrap your page

layout.tsx- persists; errors bubble to parenterror.tsx- catches page and child errorsloading.tsx- Suspense fallback for the pagepage.tsx- your route content renders here
fig · How special files wrap a route segment

Adding loading.tsx wraps that segment's page in a <Suspense>, so its fallback shows at once while the server streams the page in. error.tsx is a React error boundary for the segment and must start with 'use client'; it catches render errors in the page and its nested children. not-found.tsx renders when code calls notFound(). A template, when present, sits inside the layout and wraps these files.

[ senior depth ]

Composition patterns that keep use client at the leaves

The slot pattern is the workhorse: a Client Component renders {children}, and a Server parent passes server content into it. A modal that toggles with client state can wrap a <Cart /> that reads the database, because <Cart /> renders on the server and reaches the modal as output. Context providers take the same shape: a 'use client' provider that accepts children, rendered in a Server layout as deep as possible so static parts above it stay static. For a third-party component that uses client features but ships no directive of its own, re-export it from your own 'use client' file. To keep secrets or a DB client out of the browser bundle, import the server-only package in that module; it throws at build time if a Client Component pulls it in.

Multiple root layouts, and why a layout cannot read the request

Any layout with none above it is a root layout, and route groups let you keep several: app/(shop)/layout.tsx and app/(marketing)/layout.tsx each render their own <html> and <body>. Moving between routes under different root layouts forces a full page reload rather than a client transition, and two groups resolving to the same path are a build error. Layouts are cached and never re-render across navigation, which is what preserves client state inside them; the price is that they cannot see per-request data such as searchParams or pathname. Read those in a Client child, or take searchParams from the page's props.

Where an error boundary sits, and what loading skips

Wrong "A segment's error.tsx catches whatever throws in that segment." It does not catch the segment's own layout.tsx or template.tsx; those errors bubble to the parent boundary, and a root-layout error needs global-error.tsx, which replaces the root layout and renders its own <html> and <body>. error.tsx must be a Client Component, since error boundaries rely on lifecycle that runs only in the browser. It wraps the page and nested children and catches their render errors, but skips event-handler and after-render async errors (use try/catch there), and treats notFound() / redirect() as control flow rather than errors. loading.tsx has a matching gap: it wraps the page in Suspense, though not the same-segment layout, so a layout that awaits cookies() or an uncached fetch blocks the fallback until it resolves. Move that work into the page, or give the layout its own <Suspense>.

'use client' // error boundaries must be Client Components
export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
  return <button onClick={() => reset()}>Try again</button> // reset re-renders the segment
}

On Next 15 the props are { error, reset }. Next 16.2 added unstable_retry (it re-fetches and re-renders, where reset only re-renders) and now recommends it. In production, Server Component error text is stripped to a generic message plus a digest that matches the server log.

Next 15 turned caching and request APIs into opt-in

Next 15 flipped the caching defaults. fetch() is no longer cached by default (opt in with { cache: 'force-cache' } or { next: { revalidate } }), GET Route Handlers are dynamic unless you set export const dynamic = 'force-static', and the Client Router Cache uses staleTime = 0 for page segments, so each navigation reflects fresh server data. The request APIs went async: cookies(), headers(), draftMode(), and the params and searchParams props are Promises you await (or read with use() in a Client Component). The App Router runs on React 19. Next 16's Cache Components model shifts streaming behavior again, but it is opt-in and outside the Next 15 baseline.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

unlocks

was this useful?