# Next.js from Zero - React Grows a Server

> What Next.js adds to React and why - file routing, server components, data fetching, caching - explained from the request up, so the framework stops feeling like magic conventions.


---

# Next.js from Zero - React Grows a Server

You know React. Then you open a Next.js project and the rules seem to have changed: components run
on a server now, some files are magically routes, `useState` throws an error in one file and works
fine in the next, and pages are somehow stale until you sprinkle the right incantation. None of it
is magic - all of it follows from one move: **Next.js puts a server in front of your React app**,
and everything the framework does is about deciding what happens on that server versus in the
browser.

This guide builds that model from the request up. By the end, "why is this a server component," "why
is this page cached," and "why did hydration fail" all have answers you can reason to, not memorize.

> ⏭️ New to React itself? Read [React from Zero](../react-from-zero/_guide.md) first - this guide
> assumes components, props, state, and effects are already solid.

## How to read this

- **In a panic right now?** Jump to [Phase 7: When Next.js Breaks](07-when-it-breaks.md) and use the
  cheat-card at the top.
- **Want it to finally make sense?** Read in order - the server/client split in Phase 3 is the hinge
  the whole framework turns on.

## The phases

1. **[What Next.js Actually Is](01-what-nextjs-actually-is.md)** - what a plain React app can't do,
   and what putting a server in front of it buys you.
2. **[Routing with Files](02-routing-with-files.md)** - folders become URLs; `page`, `layout`, and
   dynamic segments.
3. **[Server and Client Components](03-server-and-client-components.md)** - the big split: what runs
   where, and what `'use client'` really marks.
4. **[Data on the Server](04-data-on-the-server.md)** - async components, `loading.tsx`, streaming,
   and error boundaries.
5. **[Mutations: Forms and Server Actions](05-mutations-and-server-actions.md)** - writing data
   without hand-building an API layer.
6. **[Static, Dynamic, and the Cache](06-static-dynamic-and-the-cache.md)** - why your page is stale
   (or slow), and how Next decides at build time.
7. **[When Next.js Breaks](07-when-it-breaks.md)** - hydration mismatches, hook errors, serialization
   walls, and stale pages, decoded.
8. **[Where to Go Next](08-where-to-go-next.md)** - deployment, metadata, images, and what to ignore.

> Deliberately deferred to follow-up guides: authentication patterns, advanced caching internals,
> parallel/intercepting routes, middleware-heavy architectures, and testing. Core model first.


---

# What Next.js Actually Is

A plain React app (the Vite kind you built in React from Zero) has a specific shape, and that shape
has specific costs. Next.js exists because of those costs. Before touching the framework, look
squarely at what it's fixing - otherwise every Next convention feels like ceremony.

## What a plain React app actually ships

Build a Vite React app and look at what the server sends the browser:

```html
<!doctype html>
<html>
  <body>
    <div id="root"></div>
    <script src="/assets/index-BX7z3kQ9.js"></script>
  </body>
</html>
```

That's it. An empty div and a script tag. Everything the user sees is built *in their browser*,
after the JavaScript downloads, parses, and runs, and after that JavaScript fetches whatever data it
needs. This is a **single-page application** (SPA), and the sequence looks like:

```mermaid
sequenceDiagram
  Browser->>Server: GET /products
  Server-->>Browser: empty HTML + JS bundle
  Browser->>Browser: run React, render shell
  Browser->>API: fetch product data
  API-->>Browser: JSON
  Browser->>Browser: render actual content
```

Four steps before content appears, two of them network round-trips. The costs:

- **Slow first paint on slow devices and networks.** The user stares at a blank page (or a spinner)
  while the bundle loads and the data fetch completes.
- **Search engines and link previews see an empty div.** Crawlers have gotten better at running
  JavaScript, but "sometimes, eventually, partially" - and social-card scrapers mostly don't try.
  For content that lives on being found, that's disqualifying.
- **Your data layer is public.** Every fetch happens in the browser, so every API endpoint, and the
  tokens to call it, are visible to anyone who opens DevTools.

For an app behind a login (a dashboard, an editor), these costs are often fine, and a SPA is a
perfectly good architecture. For anything public - a store, a blog, a marketing site, this site -
they hurt.

## The move: render on the server first

Next.js's core move is old, and that's a compliment: like PHP or Rails, it runs your code on a
server and sends **finished HTML**. The difference from PHP is *what* runs: your same React
components. Next renders your component tree to HTML on the server, sends that, and the content is
on screen after one round-trip:

```mermaid
sequenceDiagram
  Browser->>NextServer: GET /products
  NextServer->>DB: query products
  DB-->>NextServer: rows
  NextServer-->>Browser: full HTML, content included
  Browser->>Browser: hydrate: JS attaches to existing HTML
```

📝 **Terminology:** that last step is **hydration** - the browser downloads the component
JavaScript, React renders the tree in memory, and instead of creating DOM it *attaches* to the HTML
that's already there, wiring up event handlers. The page was readable before hydration; it becomes
*interactive* after. (Phase 7 covers what happens when the server HTML and the client render
disagree - the famous hydration mismatch.)

💡 **Key point:** Next.js is a server wrapped around React. HTML is produced on the server - either
per-request or ahead of time at build (phase 6) - and the browser's JavaScript takes over from
there. Every Next feature in this guide is a consequence of having that server: file routing
(phase 2), components that run only server-side (phase 3), data fetching without a public API
(phase 4), form handling on the server (phase 5), caching (phase 6).

## Hello, actual project

```console
$ npx create-next-app@latest my-shop
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use App Router? (recommended) … Yes
Creating a new Next.js app in ./my-shop

$ cd my-shop && npm run dev

   ▲ Next.js
   - Local:  http://localhost:3000
 ✓ Ready
```

*What just happened:* the scaffold created an `app/` directory (your routes and components), a
`public/` directory (static files), and config. `npm run dev` started the Next server - note that
word: unlike Vite's static-file dev server, this is the actual server architecture you'll deploy,
running your components on Node.

The one file worth reading immediately is `app/page.tsx` - it's the component behind `/`, and it
looks like the React you already know. Two phases from now you'll understand exactly which parts of
it ran on the server before your browser saw anything.

⚠️ **Gotcha:** "App Router" in that prompt refers to the current routing system built on the `app/`
directory - the one this guide teaches. Older tutorials (and many codebases) use the previous
system, the "Pages Router" (`pages/` directory, `getServerSideProps`). The concepts rhyme but the
APIs are entirely different; if a tutorial mentions `getStaticProps`, it's teaching the old system.

## So do you need it?

An even-handed table, because "always use a framework" is tool-brain:

| Situation | Plain React (Vite SPA) | Next.js |
|---|---|---|
| Internal dashboard behind a login | ✓ simpler, no server to run | overkill unless you want its DX |
| Public content site, store, blog | slow first paint, weak SEO | ✓ this is the core case |
| You need server-side secrets in the data path | build a separate API | ✓ built in |
| Team knows React, no ops appetite | ✓ static hosting is trivial | needs a Node server or a platform |
| Embedded widget inside another site | ✓ | wrong shape entirely |

## Recap

1. A SPA ships an empty div and builds everything in the browser: fine behind a login, costly for
   public content (first paint, SEO, exposed data layer).
2. Next.js renders your React components to HTML on a server first; the browser hydrates that HTML
   into an interactive app.
3. Everything Next adds - routing, server components, actions, caching - flows from having that
   server in front.
4. App Router (`app/` directory) is the current system and what this guide teaches; `pages/` +
   `getServerSideProps` is the older one you'll meet in legacy code.

```quiz
[
  {
    "q": "Why does a plain React SPA tend to show a blank page or spinner before content appears?",
    "choices": [
      "React is slower than other frameworks at rendering",
      "The server sends an empty shell, so content waits for the JS bundle to load and the data fetch to finish",
      "Browsers block rendering until all JavaScript is downloaded",
      "The virtual DOM must be built twice on first load"
    ],
    "answer": 1,
    "why": [
      "Rendering speed isn't the bottleneck - the network round-trips before rendering can even start are.",
      null,
      "Browsers happily render HTML while scripts load - but an empty div has nothing to render.",
      "There's no double build in a SPA; that's a garbled version of hydration, which only exists with server rendering."
    ],
    "explain": "The SPA sequence is: empty HTML, download JS, run React, fetch data, then render. Server rendering collapses that into one round-trip that already contains content."
  },
  {
    "q": "What does hydration mean in a Next.js app?",
    "choices": [
      "The server refreshes stale cached pages in the background",
      "Client JavaScript attaches React to the server-sent HTML, making it interactive",
      "CSS is inlined into the HTML to avoid a second request",
      "Data is streamed into the page as it loads"
    ],
    "answer": 1,
    "why": [
      "Background refresh of cached pages is revalidation - a phase 6 topic, unrelated to hydration.",
      null,
      "CSS inlining is a build optimization; hydration is about behavior, not styling.",
      "Streaming is real (phase 4) but it's about delivering HTML in pieces, not attaching interactivity."
    ],
    "explain": "The HTML arrives readable but inert. Hydration is React rendering in memory and adopting that existing DOM, wiring up handlers - readable first, interactive after."
  }
]
```


---

# Routing with Files

In a Vite SPA you'd install React Router and declare routes in code. Next.js replaced that
configuration with a convention: **the `app/` folder structure *is* the router**. Folders are URL
segments; special filenames say what each segment renders. Learn about six filenames and you can
read any Next project's URL space by looking at its file tree.

## Folders are URLs, page.tsx is the content

```text
app/
  page.tsx                →  /
  about/
    page.tsx              →  /about
  products/
    page.tsx              →  /products
    [id]/
      page.tsx            →  /products/42, /products/tea-kettle, ...
```

A folder only becomes a *visitable* URL when it contains a `page.tsx` (or `.jsx`). A folder without
one is just organization. The component in `page.tsx` is what renders at that URL:

```tsx
// app/about/page.tsx
export default function AboutPage() {
  return <h1>About us</h1>;
}
```

*What just happened:* the default export of `page.tsx` is an ordinary React component. Visit
`/about` and the Next server renders it (server-side first, per phase 1) into the layout stack
around it. No route table, no registration - the file's existence is the registration.

## Dynamic segments: [id]

Square brackets make a folder match *any* value in that position, and your page receives it:

```tsx
// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
  const { id } = await params;
  return <h1>Product {id}</h1>;
}
```

*What just happened:* `/products/42` renders this component with `id` equal to `"42"` (always a
string - parse it if you need a number). `params` arrives as a Promise in current Next versions,
hence the `await` - and yes, the component itself is `async`, which is a server-component
superpower phase 4 explains properly.

📝 **Terminology:** `[id]` is a **dynamic segment**. The catch-all variant `[...slug]` matches any
*depth* (`/docs/a/b/c` → `slug: ['a','b','c']`) - useful for docs sites and CMS-driven trees.

## layout.tsx: the persistent frame

Every folder can also have a `layout.tsx`. A layout wraps every page at its level *and below*:

```tsx
// app/layout.tsx - the root layout, required
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <nav>...site nav...</nav>
        {children}
      </body>
    </html>
  );
}
```

Layouts nest: `/products/42` renders the root layout, then `app/products/layout.tsx` if it exists,
then the page - outermost to innermost, like nesting dolls.

```mermaid
flowchart TD
  RL[app/layout.tsx] --> PL[app/products/layout.tsx]
  PL --> PG["app/products/[id]/page.tsx"]
```

💡 **Key point:** the thing that makes layouts more than a wrapper component: **layouts don't
re-render when you navigate between their children.** Go from `/products/42` to `/products/43` and
the product layout (its nav, its sidebar, its state) stays mounted - only the page swaps. That's
the interactive-app feel with URL-per-view semantics, and it's why putting a search box's state in
a layout survives navigation, while putting it in a page doesn't.

The other members of the special-file family, briefly - each applies to its segment and below:

| File | Role |
|---|---|
| `page.tsx` | the content; makes the URL visitable |
| `layout.tsx` | persistent frame around children |
| `loading.tsx` | instant fallback while the page's data loads (phase 4) |
| `error.tsx` | error boundary for this subtree (phase 4) |
| `not-found.tsx` | rendered by `notFound()` and unmatched URLs |
| `route.ts` | a raw HTTP endpoint instead of a page (phase 5) |

## Navigating: Link, not a href

```tsx
import Link from 'next/link';

<Link href="/products/42">Mechanical keyboard</Link>
```

A plain `<a href>` works, but it does a *full page load* - throwing away every bit of client state
and re-running hydration from scratch. `Link` renders a real `<a>` (right-click, open in new tab,
everything works) but intercepts the click and does a **client-side transition**: fetch the new
page's server-rendered payload, swap it into the layout stack, keep everything else mounted. It also
prefetches routes for links scrolled into view, which is why Next navigation feels instant.

For navigating from code (after a form submit, say), the client-side hook:

```tsx
'use client';
import { useRouter } from 'next/navigation';

const router = useRouter();
router.push('/thanks');
```

⚠️ **Gotcha:** that import is `next/navigation`. The near-identical `next/router` is the old Pages
Router API - importing it in an `app/` project throws at runtime. Tutorials older than the App
Router send people into this wall constantly. (The `'use client'` line at the top gets its full
explanation next phase.)

## Recap

1. Folders are URL segments; `page.tsx` makes a segment visitable; the file tree is the route table.
2. `[id]` captures a dynamic segment into `params` (a Promise - await it); `[...slug]` captures
   arbitrary depth.
3. Layouts wrap children persistently - they don't re-render on navigation between their children,
   so their state survives.
4. `Link` = client-side transition + prefetch; a raw `<a>` = full reload. `useRouter` comes from
   `next/navigation`, never `next/router`.

```quiz
[
  {
    "q": "A search input lives in app/products/layout.tsx. The user types a query, then clicks from one product page to another. What happens to the input's text?",
    "choices": [
      "It's cleared, because navigation re-renders everything under the root layout",
      "It survives, because layouts stay mounted when navigating between their children",
      "It's cleared unless the input is a controlled component",
      "It survives only if the two pages share the same params"
    ],
    "answer": 1,
    "why": [
      "That's exactly what layouts exist to prevent - only the page portion swaps on navigation.",
      null,
      "Controlled or not, what matters is whether the component owning the state stays mounted - and the layout does.",
      "Params identify the page, not the layout; the layout persists across different params at its level."
    ],
    "explain": "Layouts don't re-render on child navigation. State in a layout survives page-to-page moves within its subtree; state in a page dies with the page."
  },
  {
    "q": "You add app/dashboard/settings/ with a component in settings.tsx inside it, and /dashboard/settings gives a 404. Why?",
    "choices": [
      "The folder needs a route.ts to register the URL",
      "The file must be named page.tsx - only that name makes a folder visitable",
      "Nested folders need their own layout.tsx first",
      "The dev server needs a restart to pick up new routes"
    ],
    "answer": 1,
    "why": [
      "route.ts creates a raw HTTP endpoint, not a page - and nothing needs registering; the filename is the registration.",
      null,
      "Layouts are optional at every level - pages render into the nearest ancestor layout.",
      "The dev server picks up new files on the fly; the name is the problem."
    ],
    "explain": "Only the special filenames mean anything to the router. A component file with any other name is just code; page.tsx is what publishes the folder as a URL."
  }
]
```


---

# Server and Client Components

This is the phase where Next.js stops being "React with routing." The App Router is built on React
Server Components, and the first time `useState` throws
`You're importing a component that needs useState... add the "use client" directive`, the framework
feels like it broke your React. It didn't - it split it. This phase makes the split precise, because
every confusing Next error for the rest of your career traces back to it.

## The default: components that only run on the server

In the `app/` directory, **every component is a server component until you say otherwise.** A server
component runs *once*, during the server render (phase 1), produces its bit of HTML-to-be, and then
is done. Its code is **never shipped to the browser.**

That "never shipped" is not a footnote - it's the point:

```tsx
// app/products/page.tsx - server component, and it can prove it
import { db } from '@/lib/db';           // the real database client
import { formatPrice } from 'heavy-money-lib';

export default async function ProductsPage() {
  const products = await db.query('SELECT * FROM products'); // secrets stay here
  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name} - {formatPrice(p.cents)}</li>)}
    </ul>
  );
}
```

*What just happened:* a component queried the database directly. No API route, no fetch, no exposed
endpoint - this code runs where the database is reachable and the credentials live, and the browser
receives only the resulting HTML. The heavy formatting library adds zero bytes to the client bundle
for the same reason. This is what phase 1's "your data layer is public" cost looks like when it's
fixed.

What a server component *can't* do follows from *when* it runs. It renders once on the server;
there's no "later" for it. So:

- **No state, no effects** - `useState`/`useEffect` are about re-rendering over time in a browser;
  a run-once render has no time axis.
- **No event handlers** - there's no user on the server to click anything.
- **No browser APIs** - `window`, `localStorage`, `document` don't exist in Node.

## 'use client': the interactivity boundary

When you need any of those, you declare it:

```tsx
// components/AddToCart.tsx
'use client';

import { useState } from 'react';

export function AddToCart({ productId }) {
  const [added, setAdded] = useState(false);
  return (
    <button onClick={() => setAdded(true)}>
      {added ? 'In cart ✓' : 'Add to cart'}
    </button>
  );
}
```

📝 **Terminology:** `'use client'` at the top of a file marks everything in that file - **and every
module it imports** - as **client components**: the React you already know, hydrated and interactive
in the browser. The directive isn't per-component, it's a *boundary marker*: it declares "from this
import edge inward, ship the JavaScript."

💡 **Key point:** client components *also* render once on the server, to produce the initial HTML
(that's hydration's other half). "Client component" doesn't mean "skips server rendering" - it means
"its code ships to the browser and comes alive there." Which is why browser-only code still needs an
effect guard even inside client components; phase 7 shows that failure.

## Composition: islands of interactivity

The design that falls out: server components form the static body of the page, with client
components as interactive islands wherever behavior is needed.

```mermaid
flowchart TD
  P[ProductsPage - server] --> L[ProductList - server]
  P --> S[SearchBox - client 🏝]
  L --> C1[ProductCard - server]
  C1 --> A[AddToCart - client 🏝]
```

Two composition rules, both consequences of "server code never reaches the browser":

1. **A client component can't import a server component.** Once you're inside the client boundary,
   everything imported ships to the browser - a database-touching component can't. But a client
   component *can receive server-rendered children*: `<ClientTabs>{serverRenderedContent}</ClientTabs>`
   works, because the server content is passed as an already-rendered prop, not imported. When you
   need a server thing *inside* a client thing, pass it as `children`.
2. **Props crossing the boundary must be serializable.** They travel from the server render to the
   browser, so they have to survive the trip as data: strings, numbers, booleans, plain
   objects/arrays, null. Functions, class instances, and `Date` objects (in their raw form) can't
   cross - which is exactly what the error
   `Functions cannot be passed directly to Client Components` is telling you. (The one exception,
   server actions, is phase 5's whole topic.)

## Where to draw the line

The practical craft is pushing the boundary *down*:

```tsx
// ✗ page-level 'use client' - the whole page ships as JS, DB access now impossible
// ✓ server page, with a small client island for the one interactive part
export default async function ProductPage({ params }) {
  const { id } = await params;
  const product = await db.product(id);       // server: data, secrets, zero JS shipped
  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <AddToCart productId={product.id} />     {/* client: the island */}
    </article>
  );
}
```

The lazy failure mode is slapping `'use client'` at the top of every file until errors stop - it
works, and it quietly turns your app back into the SPA from phase 1, shipping everything to the
browser and cutting you off from direct data access. When an error demands `'use client'`, the right
question is "what's the *smallest* subtree that truly needs interactivity?"

## Recap

1. Default is server: runs once at render, can touch databases and secrets, ships no JS, can't use
   state/effects/handlers.
2. `'use client'` marks a boundary file: that module and its imports ship to the browser and hydrate.
3. Client components still server-render their initial HTML - the directive is about shipping code,
   not skipping the server.
4. Server content goes *inside* client components via `children`, never via import; props crossing
   the boundary must be serializable.
5. Push the boundary down: server body, small client islands.

```quiz
[
  {
    "q": "Adding useState to app/dashboard/page.tsx throws an error mentioning \"use client\". What is the actual conflict?",
    "choices": [
      "Pages are special files that can never hold state",
      "The component is a server component, which renders once on the server - there's no re-render lifecycle for state to drive",
      "useState must be imported from next/navigation inside the app directory",
      "State in a page would break the router's caching"
    ],
    "answer": 1,
    "why": [
      "Pages aren't special here - add 'use client' and the same page can hold state (at the cost of becoming a client subtree).",
      null,
      "useState always comes from react; next/navigation is for routing hooks.",
      "Caching interacts with rendering modes (phase 6), not with whether a component holds state."
    ],
    "explain": "State means re-rendering over time in a browser. A server component has no time axis - it runs once and is done - so the hook has nothing to attach to."
  },
  {
    "q": "Why does marking a small, frequently-used component file with 'use client' potentially affect much more than that one component?",
    "choices": [
      "The directive is contagious upward: its parents become client components too",
      "Everything that file imports is pulled into the client bundle along with it",
      "It disables server rendering for the whole route",
      "It forces every sibling component to hydrate first"
    ],
    "answer": 1,
    "why": [
      "It spreads down through imports, not up through parents - a server page can happily render client islands.",
      null,
      "Client components still server-render their initial HTML; nothing about the route's rendering is disabled.",
      "Hydration order isn't governed by the directive; bundle contents are."
    ],
    "explain": "'use client' marks a boundary: that module plus its entire import graph ships to the browser. A heavy import in a casually-marked file is a heavy addition to the bundle."
  },
  {
    "q": "A client component needs to display a chunk of UI that queries the database. What's the working pattern?",
    "choices": [
      "Import the server component inside the client component",
      "Have the parent server component render it and pass it in as children",
      "Mark the database component 'use client' so they match",
      "Fetch the data in a useEffect instead"
    ],
    "answer": 1,
    "why": [
      "Importing pulls it into the client bundle - where database code cannot go; this is the exact forbidden direction.",
      null,
      "That ships DB access code to the browser - it will fail to build, and it would be a security hole if it didn't.",
      "It works, but it recreates the SPA waterfall and a public API requirement - the costs Next exists to remove; use it when client-side freshness is genuinely needed, not as the default."
    ],
    "explain": "Server content crosses into client subtrees as already-rendered props (children), never as imports. <ClientFrame>{await serverThing()}</ClientFrame> is the shape."
  }
]
```


---

# Data on the Server

In React from Zero, fetching meant a `useEffect`, three pieces of state (`data`, `loading`,
`error`), a cancellation flag, and care. That machinery existed because a browser component can only
get data *after* it mounts. A server component has no such problem: it runs on the server, where the
data lives, *before* any HTML exists. So fetching collapses into the most boring possible code -
and the interesting questions move to "what does the user see while it's happening?"

## Fetching is just awaiting

```tsx
// app/orders/page.tsx
import { db } from '@/lib/db';

export default async function OrdersPage() {
  const orders = await db.orders.list();     // or: await fetch('https://api...').then(r => r.json())
  return (
    <table>
      {orders.map(o => <OrderRow key={o.id} order={o} />)}
    </table>
  );
}
```

*What just happened:* the component is an `async` function, so it can `await` anything - a database
call, an ORM, `fetch` against an external API. React waits for the promise, then renders the result
into the HTML response. No effect, no loading state, no race conditions - there's exactly one run,
and it has the data before it returns.

💡 **Key point:** the three-state fetch dance (`data`/`loading`/`error` in `useState`) is a *client*
pattern. On the server it dissolves: awaiting **is** loading, throwing **is** error, and both get
first-class UI files below. Reach for the client pattern only when the data must refresh *in the
browser* without navigation (live search, polling) - and then preferably via a library like TanStack
Query, per React from Zero phase 9.

Two components that need the same data? Fetch in both. Next deduplicates identical `fetch` calls
within a single render pass (and for non-fetch sources, React's `cache()` wraps a function with the
same per-render memoization), so you don't thread props through five layers just to share a query.

## loading.tsx: what renders while you await

A fair question about that `await`: the server can't send the finished page until the data
arrives, so what does the user stare at? Without help: the previous page, feeling frozen. The fix is
the `loading.tsx` special file from phase 2's table:

```tsx
// app/orders/loading.tsx
export default function OrdersLoading() {
  return <TableSkeleton rows={8} />;
}
```

*What just happened:* Next now responds **immediately** with the layout stack plus this skeleton,
keeps the connection open, and **streams** the real page content into place when the await
resolves. The user sees structure instantly and content moments later - no frozen click, no blank
page.

📝 **Terminology:** under the hood, `loading.tsx` wraps your page in a React **Suspense boundary** -
the mechanism that lets a tree say "this part isn't ready; show the fallback, swap in the content
when it resolves." The file is the convenient spelling; the mechanism is Suspense.

## Streaming the slow part, not the whole page

`loading.tsx` gates the *entire* page behind its slowest await. When one widget is slow and the rest
is fast, put the boundary around just the slow thing:

```tsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <>
      <QuickStats />                                {/* fast: renders in the first flush */}
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />                            {/* slow query: streams in when ready */}
      </Suspense>
    </>
  );
}
```

```mermaid
sequenceDiagram
  Browser->>Server: GET /dashboard
  Server-->>Browser: layout + QuickStats + ChartSkeleton
  Server->>DB: slow revenue query
  DB-->>Server: rows
  Server-->>Browser: RevenueChart streamed into place
```

*What just happened:* one HTTP response, delivered in chunks. The fast content painted immediately;
the slow island arrived when its data did. This is the server-side answer to "spinner city" - each
slow region gets its own boundary instead of the whole page waiting on the slowest query.

⚠️ **Gotcha:** the await has to live *inside* the Suspense boundary to stream. If `Dashboard` itself
awaits the slow query and passes rows down to `RevenueChart`, the whole page waits - the boundary
can only cut off what suspends *within* it. Move the fetch into the component behind the fallback.

## error.tsx: when the await throws

Data code fails: the API times out, the row doesn't exist. The `error.tsx` file is the route
subtree's error boundary:

```tsx
// app/orders/error.tsx
'use client';   // error boundaries must be client components (they use state to recover)

export default function OrdersError({ error, reset }) {
  return (
    <div>
      <h2>Couldn't load your orders.</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}
```

*What just happened:* an uncaught throw anywhere under `/orders` renders this instead of the page -
the layout stack above it stays intact, so the site chrome survives the crash. `reset` re-renders
the failed subtree for a retry. The mandatory `'use client'` isn't an exception to phase 3 - error
recovery is interactivity, and interactivity is client work.

For the specific case of "this thing doesn't exist," throw the router's own signal instead:

```tsx
import { notFound } from 'next/navigation';

const product = await db.product(id);
if (!product) notFound();   // renders the nearest not-found.tsx with a 404 status
```

A real 404 status matters: it tells crawlers "don't index this," where a styled "not found" page
returning 200 tells them the opposite - the SEO lesson hiding inside an error-handling API.

## Recap

1. Server components fetch by awaiting - one run, data before render, no loading/error state
   machinery.
2. Duplicate fetches within a render are deduplicated (`fetch` automatically, other sources via
   `cache()`) - fetch where you need, don't prop-thread.
3. `loading.tsx` streams the page: instant skeleton, content swapped in when ready. It's Suspense
   with a filename.
4. Wrap just the slow islands in `<Suspense>` - and put the await inside the boundary, or nothing
   streams.
5. `error.tsx` catches throws per subtree (always `'use client'`); `notFound()` gives missing
   resources a real 404.

```quiz
[
  {
    "q": "Why doesn't fetching in a server component need the data/loading/error useState pattern?",
    "choices": [
      "Next.js manages those three states automatically behind the scenes",
      "The component runs once with await - it has the data before it renders, so there are no in-browser states to track",
      "Server components are faster, so loading states never appear",
      "The pattern still applies; it's only written in a different file"
    ],
    "answer": 1,
    "why": [
      "Nothing is managed invisibly - the states genuinely don't exist, because there's no mounted component waiting for data.",
      null,
      "Speed isn't the reason - a slow query still takes time; that time is just handled by streaming (loading.tsx), not component state.",
      "loading.tsx and error.tsx replace the pattern's UI, but the state machinery itself is gone, not relocated."
    ],
    "explain": "The client pattern exists because browsers mount first and fetch after. A server component awaits first and renders once - loading UI comes from Suspense boundaries, errors from error.tsx."
  },
  {
    "q": "A dashboard page awaits a slow query at the top and passes the result into a component wrapped in <Suspense>. The whole page still waits for the query. Why?",
    "choices": [
      "Suspense only works with the loading.tsx file, not inline",
      "The await happens outside the boundary - the page suspends before Suspense can isolate anything",
      "The fallback component was too heavy to stream",
      "Streaming requires the Edge runtime"
    ],
    "answer": 1,
    "why": [
      "Inline <Suspense> works exactly like loading.tsx - the file is sugar for the same boundary.",
      null,
      "Fallback weight affects paint time, not whether streaming happens at all.",
      "Node streams these responses fine; no special runtime is involved."
    ],
    "explain": "A boundary can only cut off work that suspends inside it. Move the fetch into the component behind the fallback, and the rest of the page flushes immediately."
  }
]
```


---

# Mutations: Forms and Server Actions

Reading data was phase 4. Writing it - the form submit, the delete button, the "add to cart" - used
to require building an API route, fetching it from the client, and hand-managing the states in
between. Server actions collapse that: **you write a function, and the framework builds the
endpoint.** It's the feature that looks most like magic and is most worth de-mystifying, because
under the hood it's the plainest thing in Next: a POST request with a name on it.

## What a server action actually is

```tsx
// app/todos/actions.ts
'use server';

import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

export async function addTodo(formData: FormData) {
  const text = String(formData.get('text') ?? '').trim();
  if (!text) return;                       // validate on the server - the only place it counts
  await db.todos.insert({ text });
  revalidatePath('/todos');                // more on this below
}
```

```tsx
// app/todos/page.tsx - a server component with a form
import { addTodo } from './actions';

export default async function TodosPage() {
  const todos = await db.todos.list();
  return (
    <>
      <form action={addTodo}>
        <input name="text" />
        <button>Add</button>
      </form>
      <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>
    </>
  );
}
```

*What just happened:* `'use server'` marks the file's exports as **server actions** - functions that
always execute on the server, no matter where they're called from. When you pass one to a form's
`action`, the framework registers a private endpoint for it; submitting the form POSTs the form data
there, runs your function server-side, and re-renders the page with the result. You wrote zero API
code, zero fetch code, and the database call never left the server.

📝 **Terminology:** this is the one legal way a *function* crosses the server/client boundary from
phase 3 - because what actually crosses is not the function but a **reference** to it, which the
framework resolves back to server code when invoked.

⚠️ **Gotcha:** `'use server'` does not mean "server component" - components are server-rendered *by
default* and need no directive. It means "callable-from-anywhere function that runs on the server."
Two directives, two meanings: `'use client'` marks a bundle boundary; `'use server'` marks remote
callability. And because every server action **is a public HTTP endpoint** (anyone can POST to it),
validation and auth checks belong *inside the action*, every time - the form is a convenience, not a
gate.

## Why this beats the fetch-an-API version

The old shape - `onSubmit` handler, `fetch('/api/todos', ...)`, JSON parsing, then manually
re-fetching the list - had four moving parts to keep aligned. The action version has one function.
And the `<form action={...}>` spelling has a property the fetch version can't offer: it works as a
plain HTML form. JavaScript still loading on a slow connection? Disabled? The form still POSTs and
the action still runs - the enhanced behavior (no page reload, streamed re-render) layers on when
hydration completes.

## revalidatePath: telling the cache what you changed

That `revalidatePath('/todos')` line is doing real work. Next caches rendered pages (the whole
machinery is phase 6), and a mutation makes cached copies *wrong* - you inserted a row, but the
cached `/todos` still shows the old list. `revalidatePath` marks that path's cached data stale so
the next look re-renders it fresh. Its sibling `revalidateTag` invalidates by tag across many pages
at once (tag your fetches with `{ next: { tags: ['todos'] } }`, then `revalidateTag('todos')`).

💡 **Key point:** mutation without revalidation is the source of the "I saved it but the page shows
the old data" class of bug. The reflex to build: every action that writes ends by declaring what it
invalidated.

## Pending state and results: useActionState

Forms need feedback - a disabled button while submitting, an error message when validation fails.
The client-side hook `useActionState` wraps an action with exactly that:

```tsx
'use client';
import { useActionState } from 'react';
import { addTodo } from './actions';       // action returns { error?: string } now

export function AddTodoForm() {
  const [state, formAction, pending] = useActionState(addTodo, { error: undefined });
  return (
    <form action={formAction}>
      <input name="text" />
      <button disabled={pending}>{pending ? 'Adding…' : 'Add'}</button>
      {state.error && <p className="error">{state.error}</p>}
    </form>
  );
}
```

*What just happened:* the hook gives you the action's last return value (`state`), a wrapped action
for the form, and a `pending` boolean during the round-trip. The action itself changes shape
slightly: it receives `(prevState, formData)` and returns the new state - which is how server-side
validation messages travel back to the form without you building a response channel.

## Route handlers: when you actually want an API

Server actions serve *your own UI*. Sometimes you need a real HTTP endpoint - a webhook receiver, a
JSON API for a mobile app, an RSS feed. That's `route.ts` from phase 2's table:

```tsx
// app/api/todos/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const todos = await db.todos.list();
  return NextResponse.json(todos);
}
```

One folder exposes `GET`/`POST`/`PUT`/`DELETE` by exporting functions with those names. The
decision line: **consumed by your own components → server action; consumed by anything else →
route handler.** (A folder can't have both `route.ts` and `page.tsx` - a URL is either a page or an
endpoint.)

## Recap

1. A server action is a `'use server'` function the framework exposes as a private POST endpoint -
   forms call it via `action={fn}`, no API layer written.
2. It's still a public endpoint: validate and authorize inside the action, always.
3. Writes end with `revalidatePath`/`revalidateTag`, or cached pages keep showing pre-mutation data.
4. `useActionState` supplies pending state and carries server validation messages back to the form.
5. Route handlers (`route.ts`) are for external consumers; actions are for your own UI.

```quiz
[
  {
    "q": "A server action skips validation because \"the form already validates in the browser.\" What's wrong with that reasoning?",
    "choices": [
      "Browser validation doesn't run for controlled inputs",
      "The action is an HTTP endpoint anyone can POST to directly, bypassing your form entirely",
      "Server actions can't read FormData without validating it first",
      "Nothing - client validation is sufficient when using form action={}"
    ],
    "answer": 1,
    "why": [
      "Browser validation runs fine on any input - but it runs in a place the attacker controls.",
      null,
      "FormData reads happily without validation - that's precisely the danger.",
      "Client validation is UX, not security, in every architecture - actions included."
    ],
    "explain": "The form is one caller of the endpoint, not the only possible one. curl can invoke your action with anything - the server-side check is the real one."
  },
  {
    "q": "After an action inserts a row, the page still lists the old data until a hard refresh. What's missing?",
    "choices": [
      "An await on the database insert",
      "A revalidatePath (or revalidateTag) call marking the cached page stale",
      "A useEffect to re-fetch after submit",
      "The form needs method=\"POST\""
    ],
    "answer": 1,
    "why": [
      "A missing await would be a race, fixed by luck on fast databases - but the hard-refresh recovery points at caching, not timing.",
      null,
      "Re-fetching client-side patches the symptom for one visitor; the cached server render stays wrong for everyone else.",
      "Actions POST already - the method is handled by the framework."
    ],
    "explain": "Next serves the cached render until told otherwise. Mutations must declare what they invalidated: revalidatePath('/todos') makes the next request re-render fresh."
  }
]
```


---

# Static, Dynamic, and the Cache

Here's the Next.js behavior that generates the most confused bug reports: you deploy, you update the
database, and the page *still shows old data* - not for one user, for everyone, indefinitely. Or the
mirror image: a page you expected to be instant hits the server on every request. Both trace to one
decision the framework makes per route, mostly silently: **render at build time, or render per
request?** This phase makes that decision visible.

## The two rendering times

**Static rendering** happens **once, at build time** (`next build`). The result - finished HTML -
is saved and served to every visitor, as fast as a file can be served, with your server doing no
per-request work. The page is identical for everyone and as fresh as the last build.

**Dynamic rendering** happens **per request**. Every visit runs your components (phase 4's awaits
included), so the page can be personalized and is always current - at the cost of doing the work
every time.

💡 **Key point:** Next's default is **static wherever possible** - it's the better deal whenever
it's legal. A route becomes dynamic when your code does something that *can't* be known at build
time. The build output tells you what happened to each route:

```console
$ npm run build

Route (app)
┌ ○ /                      # ○ = static: prerendered at build
├ ○ /about
├ ƒ /dashboard             # ƒ = dynamic: rendered per request
└ ○ /products/[id]
```

Reading this legend after every build is the habit that catches caching surprises before users do.

## What flips a route to dynamic

A route goes dynamic when it reads something that only exists at request time:

- **`cookies()` or `headers()`** - who's asking can't be known at build.
- **`searchParams`** on a page - `/products?sort=price` differs per URL.
- **An uncached data read** - a `fetch` opted out of caching, or the route/segment marked
  `export const dynamic = 'force-dynamic'`.

That's the mechanism behind the surprise: add an innocuous `cookies()` call for a theme preference
and your fully-static page quietly becomes render-on-every-request. The build legend flips from ○
to ƒ and the framework considers that your answer.

## Dynamic segments want a guest list

What about `/products/[id]` - static or dynamic? At build time Next can't know the ids. You can
tell it:

```tsx
// app/products/[id]/page.tsx
export async function generateStaticParams() {
  const products = await db.products.list();
  return products.map(p => ({ id: String(p.id) }));
}
```

*What just happened:* at build, Next calls this, gets the list of ids, and prerenders every one of
those product pages as static HTML. Without it, the route renders on demand. The middle way is both:
prerender the top 100 products, render rarer ids on first request, cache the result.

## Revalidation: static pages that refresh themselves

Static's weakness is staleness: content changed, build didn't. The blunt fix - redeploy on every
content change - doesn't survive contact with a CMS. The real fix is **revalidation**: static, with
an expiry.

```tsx
// re-render this page in the background at most every hour
export const revalidate = 3600;
```

*What just happened:* the page is served statically as usual. Once the copy is older than 3600
seconds, the *next* visitor still gets the cached copy instantly - but triggers a background
re-render, and everyone after gets the fresh one. Old name for this: ISR, incremental static
regeneration. Practical meaning: "static speed, at most an hour stale."

The event-driven version you already met in phase 5 beats the timer version when you control the
writes: `revalidatePath`/`revalidateTag` from a server action (or a CMS webhook hitting a route
handler) refresh the page *at the moment the data changes*, no polling interval to tune.

For per-fetch control, the options ride on `fetch` itself:

```tsx
await fetch(url, { cache: 'force-cache' });            // cache indefinitely (until revalidated)
await fetch(url, { next: { revalidate: 300 } });       // this data: at most 5 minutes stale
await fetch(url, { next: { tags: ['products'] } });    // invalidatable via revalidateTag
```

## The decision, as a picture

```mermaid
flowchart TD
  Q{Reads cookies, headers,\nsearchParams, or uncached data?}
  Q -->|yes| DYN[ƒ dynamic: rendered per request]
  Q -->|no| STAT[○ static: prerendered at build]
  STAT --> R{revalidate set?}
  R -->|yes| ISR[refreshes in background after expiry]
  R -->|no| FROZEN[fresh as of the last build]
```

⚠️ **Gotcha:** the classic false diagnosis. Page shows stale data → developer adds
`force-dynamic` → page is correct now but renders on every request forever. The stale page was
*telling you* it was static without a revalidation story; the proportionate fix was `revalidate` or
a `revalidatePath` at the write site - keeping static speed *and* freshness. Reach for
`force-dynamic` when the page is genuinely per-visitor (a dashboard), not as a cache-buster.

⚠️ **Gotcha:** in development (`npm run dev`) every route renders per-request so you always see
fresh code and data. The static/stale behavior only exists in the built app - which is why "works
in dev, stale in prod" is this phase's signature bug report. Test caching behavior with
`npm run build && npm start`, never with the dev server.

## Recap

1. Static = rendered once at build, served as a file; dynamic = rendered per request. Default is
   static wherever legal.
2. `cookies()`, `headers()`, `searchParams`, and uncached reads flip a route dynamic - check the
   ○/ƒ legend after builds.
3. `generateStaticParams` prerenders dynamic segments' known values.
4. `revalidate = N` gives static pages an expiry; `revalidatePath`/`revalidateTag` refresh them the
   moment data changes.
5. Stale page ≠ reach for `force-dynamic` - that trades the caching away instead of fixing its
   freshness.

```quiz
[
  {
    "q": "A product page shows updated prices in dev but week-old prices in production. What's the most likely explanation?",
    "choices": [
      "The production database is behind the development one",
      "The route is static: it was prerendered at the last build and has no revalidation configured",
      "The CDN is ignoring cache headers",
      "generateStaticParams returned the wrong ids"
    ],
    "answer": 1,
    "why": [
      "Possible, but the works-in-dev/stale-in-prod signature points at rendering time: dev renders per request, the build doesn't.",
      null,
      "A CDN issue wouldn't respect the dev/prod split this cleanly - and Next's own cache sits before any CDN.",
      "Wrong ids would 404 or miss pages, not serve old prices on existing ones."
    ],
    "explain": "Dev renders every request; the built app serves what the build produced. A static page without revalidate stays as fresh as the last deploy - add revalidate or revalidate on write."
  },
  {
    "q": "Adding a cookies() call for a theme preference made a landing page render on every request. Why?",
    "choices": [
      "Reading cookies is slow, so Next disables caching to compensate",
      "Cookie values only exist per request, so the page can no longer be rendered once at build",
      "cookies() is only allowed in client components",
      "The theme value changes too often to cache"
    ],
    "answer": 1,
    "why": [
      "It's not a performance heuristic - it's a logical impossibility: there is no cookie at build time.",
      null,
      "cookies() is server-only - it's legal here; the cost is the rendering mode, not an error.",
      "Even a never-changing cookie has this effect; what matters is when the value becomes knowable."
    ],
    "explain": "Static rendering happens at build, where no request - and no cookie - exists. Reading request-time data forces per-request rendering. Consider reading the theme client-side to keep the page static."
  },
  {
    "q": "A marketing page's content updates in the CMS a few times a week. The team wants static-file speed. Best setup?",
    "choices": [
      "export const dynamic = 'force-dynamic' so it's always fresh",
      "Static with revalidation - a revalidate interval, or a CMS webhook calling revalidatePath",
      "A client component that fetches the content in useEffect",
      "Rebuild and redeploy the site on a nightly schedule"
    ],
    "answer": 1,
    "why": [
      "Always-fresh at the price of rendering every request - the exact overcorrection this phase warns about for content that changes twice a week.",
      null,
      "That reintroduces the SPA waterfall and hides content from crawlers - everything phase 1 left behind.",
      "It works, but couples freshness to deploy cadence and re-renders everything for one page's change - revalidation does it surgically."
    ],
    "explain": "Static with an expiry (or event-driven revalidation from the CMS webhook) is the built-for-this answer: file-serving speed, freshness within minutes of a change."
  }
]
```


---

# When Next.js Breaks

Every classic Next.js error is the server/client split from phase 3 poking through the floor. That's
genuinely good news: one mental model explains all five of the messages below, so this phase is
shorter than it looks - it's the same lesson wearing five costumes.

## The cheat-card

| Symptom / message | Almost always means | Fix |
|---|---|---|
| **"Hydration failed" / "text content does not match"** | Server HTML ≠ first client render | Find the nondeterminism: dates, random, locale, browser-only branches; render them after mount |
| **"useState/useEffect only works in a Client Component"** | Hook in a server component | `'use client'` on the *smallest* interactive subtree - not the page |
| **"window is not defined"** | Browser API running during server render | Move into `useEffect`; or `next/dynamic` with `ssr: false` |
| **"Functions cannot be passed directly to Client Components"** | Non-serializable prop crossing the boundary | Pass data, not functions - or make it a server action (phase 5) |
| **Page shows old data after a write** | Missing revalidation | `revalidatePath`/`revalidateTag` in the action (phase 5/6) |
| **Stale in prod, fine in dev** | Static route without a freshness story | `revalidate`, or revalidate-on-write (phase 6) |
| **`useRouter` throws immediately** | Imported from `next/router` in an `app/` project | Import from `next/navigation` (phase 2) |

Three of these deserve the full walk-through.

## Hydration mismatch: the flagship error

The message is alarming and the cause is almost always mundane:

```text
Error: Text content does not match server-rendered HTML.
Server: "Order placed 11/17/2026, 10:03 PM"
Client: "Order placed 17/11/2026, 22:03"
```

Remember hydration (phase 1): the browser re-renders your components and *attaches* to the
server's HTML, trusting they'll produce identical output. Anything that renders differently in the
two environments breaks that trust. The usual suspects:

- **Locale-formatted dates and numbers** - `toLocaleString()` used the server's locale, then the
  user's. (The transcript above: US server, European visitor.)
- **`Date.now()` / `Math.random()`** in render - by definition different on every run.
- **Browser-only branching** - `typeof window !== 'undefined' ? <A/> : <B/>` renders `<B/>` on the
  server and `<A/>` on the client. The check *prevents* the crash and *causes* the mismatch.
- **Invalid HTML nesting** - `<p>` inside `<p>`, `<div>` inside `<p>`: the browser's parser
  silently rearranges the server HTML, and then React's render doesn't match the rearranged DOM.

The standard fix pattern for values only the client can know correctly:

```tsx
'use client';
function LocalTime({ iso }) {
  const [text, setText] = useState(null);            // server and first client render agree: nothing
  useEffect(() => {
    setText(new Date(iso).toLocaleString());         // after mount, browser-only, no mismatch
  }, [iso]);
  return <span>{text ?? '…'}</span>;
}
```

*What just happened:* both environments render the placeholder identically, hydration succeeds,
*then* the effect fills in the locale-correct value. You'll see this pattern (or the
`suppressHydrationWarning` attribute for single unavoidable nodes, like a timestamp in a `<time>`
tag) throughout production Next codebases.

## "window is not defined"

A library (a chart, a map, an editor) reaches for `window` the moment it's imported - and phase 3
told you client components *also* render once on the server. Node has no `window`; the render
crashes before the browser ever gets a chance.

Two fixes, by scope:

```tsx
// Code you control: touch the browser only after mount
useEffect(() => {
  const saved = localStorage.getItem('draft');   // effects never run on the server
  ...
}, []);

// A whole component you don't control: skip its server render entirely
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('./Chart'), { ssr: false });
```

*What just happened:* `next/dynamic` with `ssr: false` renders nothing for that component on the
server and mounts it client-side only - the escape hatch for browser-bound libraries. Use it
per-component, not per-page; everything around the chart still gets server-rendered HTML.

## The serialization wall

```text
Error: Functions cannot be passed directly to Client Components
unless you explicitly expose it by marking it with "use server".
```

This one's error text is a complete diagnosis: a server component passed `onDelete={someFunction}`
to a client island. Props cross the boundary as *data over the wire* (phase 3), and a closure over
server scope can't be serialized into a browser. The error even names your two options:

- The child needs to trigger *server work* → make the function a **server action** (`'use server'`)
  and pass that - actions are the one function-shaped thing designed to cross (phase 5).
- The child needs *client behavior* → define the handler inside the client component and pass
  plain data (`productId`, not `deleteProduct`) down to it.

## Debugging Next: two habits

- **Ask "where did this code run?" first.** Server terminal log vs browser console log - a
  `console.log` in a server component appears in the terminal where `npm run dev` runs, not in
  DevTools. Knowing which log a line lands in *is* knowing which side of the split it's on, which is
  half of every diagnosis in this phase.
- **Reproduce cache issues in a production build.** Phase 6's rule bears repeating as a debugging
  habit: `npm run build && npm start`. The dev server's always-fresh rendering *cannot* reproduce
  staleness bugs, and DevTools' "Disable cache" checkbox doesn't touch Next's server-side caches.

## Recap

1. Hydration mismatch = server HTML and first client render disagree - hunt locale, time,
   randomness, browser-branches, and invalid nesting; render client-only truths after mount.
2. `window is not defined` = browser API at server-render time - effects for your code,
   `dynamic(..., { ssr: false })` for libraries.
3. Functions don't cross the boundary - server actions or client-local handlers do.
4. Old data after writes = missing revalidation, not a mysterious cache bug.
5. First question, always: which side of the split did this code run on?

```quiz
[
  {
    "q": "A page crashes with a hydration mismatch only for European users. The component renders new Date(order.date).toLocaleString(). What's happening?",
    "choices": [
      "European browsers parse dates differently and throw",
      "The server rendered the date in its own locale; the user's browser re-rendered it in theirs, and the outputs differ",
      "The date arrives as undefined for non-US timezones",
      "toLocaleString is not supported during server rendering"
    ],
    "answer": 1,
    "why": [
      "No browser throws on toLocaleString - the crash is React refusing mismatched HTML, not a parse error.",
      null,
      "The value arrives fine everywhere; it's the formatting of the same value that differs.",
      "It runs fine on the server - using the server's locale, which is precisely the problem."
    ],
    "explain": "Hydration requires identical output in both environments. Locale formatting is environment-dependent, so format after mount (or suppress the warning on that one node)."
  },
  {
    "q": "A server component wants a client-side DeleteButton to remove an item. Passing onDelete={() => db.items.delete(id)} throws a serialization error. The right fix?",
    "choices": [
      "Mark the DeleteButton file 'use server' so the function is allowed",
      "Make the delete a server action and pass that to the button",
      "Stringify the function and eval it client-side",
      "Move the database call into the DeleteButton's onClick"
    ],
    "answer": 1,
    "why": [
      "'use server' marks functions as remotely callable, not components - and the button is client by necessity (it has onClick).",
      null,
      "Eval'ing serialized closures is a security hole and the closure's server scope still wouldn't exist in the browser.",
      "That imports database code into the client bundle - the build fails, and it would expose credentials if it didn't."
    ],
    "explain": "Server actions are the one function-shaped thing designed to cross the boundary: the client gets a reference, invocation runs on the server."
  }
]
```


---

# Where to Go Next

You have the whole load-bearing structure now: a server in front of React, files as routes, the
server/client split, data reads that await, writes that revalidate, and a cache you can reason
about. What's left is the supporting cast - deployment, the built-in optimizations, and a short
list of things you're allowed to ignore until they're needed.

## Shipping it

`next build` produces an app that needs Node to run (`next start`) - unless every route came out
static, this isn't a folder of files you can drop on any static host. The options, plainly:

| Option | The deal |
|---|---|
| **Vercel** | Zero-config, made by the Next team, generous free tier. The trade: pricing at scale and platform coupling are the things to keep an eye on. |
| **Your own server / container** | `output: 'standalone'` in the config produces a self-contained build; run it with Node or Docker anywhere. All features work - you own scaling, caching infrastructure, and updates. |
| **Static export** | `output: 'export'` emits plain HTML/CSS/JS for static hosts - and disables everything requiring the server: actions, dynamic rendering, revalidation, image optimization. Only fits fully-static sites, at which point plain React + Vite deserved a second look. |

No righteous answer: a side project's calculus differs from a company's. The one non-negotiable is
knowing *which* features your deployment target supports before you build around them.

## Metadata: the SEO layer

Phase 1 promised crawlers real HTML; metadata is the other half of being findable. In the App
Router it's an export, not a component:

```tsx
// static pages: an object
export const metadata = {
  title: 'Handmade kettles - TeaWorks',
  description: 'Small-batch kettles, shipped worldwide.',
};

// dynamic pages: a function that can await the same data as the page
export async function generateMetadata({ params }) {
  const { id } = await params;
  const product = await db.product(id);       // deduplicated with the page's own fetch
  return { title: `${product.name} - TeaWorks`, description: product.blurb };
}
```

*What just happened:* Next renders these into `<head>` - title, description, and (via the
`openGraph` field) the social-share card data. `generateMetadata` runs server-side with full data
access, so every product page gets a real title without client tricks. Titles compose through
layouts too - a `template: '%s - TeaWorks'` in the root layout suffixes every child page.

## The built-in optimizations worth adopting early

- **`next/image`** - the `<Image>` component resizes, converts to modern formats, lazy-loads, and
  (the quietly important part) reserves layout space so images don't shove the page around while
  loading. Requires `width`/`height` (or `fill`) for exactly that reason.
- **`next/font`** - self-hosts fonts at build time: no request to a fonts CDN, no layout shift from
  late-arriving type, fonts served from your own origin.
- **`next/script`** - the `strategy` prop (`lazyOnload`, `afterInteractive`) keeps third-party
  scripts from blocking your page.

None of these is conceptually deep - they're defaults-done-right wrappers. Adopting them early
costs minutes; retrofitting them across a grown codebase costs a sprint.

## Ignorable until proven necessary

The remaining surface of Next, ranked by how safely you can defer it:

- **Middleware** - code that runs before routing on every request (auth gates, redirects, A/B
  splits). Powerful, easy to overuse; wait for a cross-cutting request-level need.
- **Parallel & intercepting routes** - advanced routing for split-pane and modal-over-page UIs.
  Genuinely clever; genuinely rare.
- **Edge runtime** - running routes on CDN-edge workers with a restricted API. A latency
  optimization with real constraints; measure before adopting.
- **Turbopack, PPR, and whatever this year's acronym is** - build-pipeline and rendering
  refinements arrive constantly. Your mental model from phases 1-7 is the durable part; let release
  notes be release notes.

## Additional resources

- [nextjs.org/docs](https://nextjs.org/docs) - the official docs; the "App Router" section maps
  one-to-one onto this guide's phases and goes deeper on each.
- [nextjs.org/learn](https://nextjs.org/learn) - the official hands-on course; a good next week of
  practice.
- [React from Zero, phase 9](../react-from-zero/09-where-to-go-next.md) - the client-side ecosystem
  map (TanStack Query, state stores) applies unchanged inside Next's client components.

## Recap

1. Deployment is a real decision: Vercel for zero-config, `standalone` for own-infrastructure,
   `export` only for fully-static sites.
2. The `metadata` export / `generateMetadata` function is the SEO layer - server-rendered, with
   data access, composed through layouts.
3. Adopt `next/image` and `next/font` from day one; they're cheap now and expensive later.
4. Middleware, parallel routes, edge, and the acronym-of-the-year can all wait for a demonstrated
   need.

```quiz
[
  {
    "q": "A team picks output: 'export' for static hosting, then finds their contact form's server action doesn't run. Why?",
    "choices": [
      "Static exports require forms to use route handlers instead",
      "Static export produces only files - there is no server, and actions need one",
      "The action was missing revalidatePath",
      "Server actions require Vercel"
    ],
    "answer": 1,
    "why": [
      "Route handlers are server code too - they're equally absent from a static export.",
      null,
      "revalidatePath governs cache freshness after a write; here the write can't execute at all.",
      "Actions run on any Node host - the constraint is having a server, not a vendor."
    ],
    "explain": "Everything from phases 4-6 that involved 'the server' - actions, dynamic rendering, revalidation - requires one to exist. output: 'export' trades all of it for static-host simplicity."
  },
  {
    "q": "Product pages need correct titles and social-share cards per product. Where does that belong?",
    "choices": [
      "A useEffect setting document.title after mount",
      "A generateMetadata function on the product page, fetching the product server-side",
      "A Head component rendered inside each page's JSX",
      "meta tags hardcoded in the root layout"
    ],
    "answer": 1,
    "why": [
      "Effects run after hydration in the browser - social-card scrapers and many crawlers never see the result.",
      null,
      "That's the Pages Router pattern (next/head); the App Router replaced it with the metadata exports.",
      "The root layout can only hold site-wide defaults - per-product values need per-page generation."
    ],
    "explain": "generateMetadata runs on the server with data access, so the title and OG tags are in the HTML itself - which is the only place scrapers reliably look."
  }
]
```
