CHAPTER 07 · FRONTEND
Rendering & Meta-Frameworks
In Chapter 06 React ran only in the browser. In real full-stack apps, the same React components also run on the server — even at build time. This chapter answers three questions: where does rendering happen, when does it happen, and how much JS is shipped to the browser? The answers decide first-screen speed, SEO, and whether your secrets stay safe.
- Compare CSR, SSR, SSG and streaming on a timeline, and pick a mode for each LinkNote page.
- Explain hydration and what causes "hydration mismatch" errors.
- Distinguish server and client components; know what
'use client'and'use server'each mark, and which one is security-sensitive. - Read the project structure of Next.js App Router and TanStack Start, and state their main trade-offs.
Opening: what is 'use client', really?
In a Next.js project, an agent wrote a component that calls await db.query(...) directly — no API layer. Later you want to add a clickable button; the agent adds 'use client' at the top of the file, and you get an error: async/await is not yet supported in Client Components, plus a warning that the database module can't be used in the browser. How can a component read the database directly? And why does one string at the top change the nature of the whole component?
Where the answer lies: in Next.js's App Router, components are Server Components by default: they execute only on the server, so they can access the database directly, and their code is never sent to the browser. 'use client' is a boundary declaration: from this file on, the component and everything it imports are bundled, shipped to the browser, and run there — and of course the browser can't connect to your database. The fix isn't to turn the whole component into a client component, but to split "the interactive button" into a small client component that the server component renders.
Three questions define a rendering mode
| Mode | Where HTML is produced | When | Typical use |
|---|---|---|---|
| CSR client-side rendering | Browser | After the user opens the page | Logged-in dashboards, highly interactive tools |
| SSR server-side rendering | Server | On every request | Personalized pages with live data |
| SSG static generation | Build machine | At build (deploy) time | Docs, blogs, marketing pages (like this guide) |
| ISR / cached rendering | Server | On first request, then cached for a while | Pages whose data changes occasionally |
| Streaming SSR | Server | On every request, sent in chunks | Pages where some data is slow |
The timeline below compares when users see content and when they can interact. Click "Play":
Line the timeline up with Chapter 05's page-load journey: CSR's step ⑤ delivers an empty shell, so there's no content until JS downloads, runs and fetches data; SSR's step ⑤ already contains the full content, visible at step ⑧, but not interactive until JS loads and "hydrates"; streaming sends the page shell first while slow parts keep computing on the server and are sent later over the same response.
Hydration: bringing server HTML to life Deep
Server-generated HTML is static markup; no event handlers are attached to its buttons. After JS loads, React renders the components again on the client with the same data, matches the result to the existing DOM, and attaches event handlers. This is hydration.
Hydration requires that the client's first render exactly matches the server's HTML. Otherwise React reports a "hydration mismatch". Common causes:
Fixes: read browser-only values in an effect after mount; let the server decide time-based values and pass them down as props; if truly necessary, mark a component as client-only. When you hit a hydration error, the first question is always: "Would this code produce different results on the server and in the browser?"
React Server Components Deep
SSR solved "see the first screen sooner" but not "too much JS": in classic SSR every component's code is still sent to the browser for hydration, even a component that only displays static text.
React Server Components (RSC) split components into two kinds:
| Server Component (default) | Client Component ('use client') | |
|---|---|---|
| Where it runs | Only on the server (or at build time) | Pre-rendered once on the server + runs in the browser |
| Code shipped to the browser? | No | Yes |
Can be async, read the DB, read secrets | ✓ | ✗ |
Can use useState, useEffect, onClick | ✗ | ✓ |
| Good for | Data fetching, layout, static content, heavy deps (Markdown rendering, syntax highlighting) | Interactivity: buttons, forms, inputs, animation |
RSC and SSR are not the same thing
This is the most common confusion in interviews and in practice. SSR means "render components to HTML on the server"; every component's code is still sent to the browser to hydrate. RSC means "some components exist only on the server"; their rendered output reaches the browser in a special serialized format, and their code is never sent. The two are usually combined: the first screen of both server and client components is output as HTML via SSR, but only client components need hydrating.
'use server': an invisible API endpoint
When a client component needs to trigger a server-side operation (like deleting a bookmark), it can call a function marked 'use server'. It looks like an ordinary function call, but the framework actually generates an HTTP endpoint for it, and the browser calls it over the network. This is a Server Function (in Next.js, often called a Server Action when used with forms).
A 'use server' function is a public network endpoint. Anyone can call it directly with any arguments, not necessarily from your UI. Every Server Function must therefore: ① validate its arguments (Zod); ② check the user is logged in; ③ check authorization (does this bookmark belong to the current user?). Agent-generated Server Actions frequently miss ③.
Also, for modules that must only run on the server (DB clients, SDKs holding secrets), add import "server-only" at the top; if a client component ever imports them, even indirectly, the build fails.
Next.js App Router Use
Next.js expresses routes through the file system. The folder structure under app/ is the URL structure, and certain file names have special meaning:
About caching: Next.js caching has gone through several significant redesigns. The direction in Next 16 is dynamic by default, cache explicitly: declare which functions or components may be cached with the "use cache" directive, and invalidate with APIs like revalidatePath / revalidateTag. Versions differ a lot here, so trust the current official docs and state your Next.js version in instructions to the agent.
TanStack Start Use
TanStack Start is built on TanStack Router and Vite. Its selling points are end-to-end type safety and being explicit: no implicit caching, and full type inference for route params, search params, loader return values, and server function inputs and outputs. It does SSR + streaming by default and behaves like an SPA on client navigation. Server Components support is being added in non-breaking steps after v1.
Note that createServerFn, like Next.js's 'use server', is essentially a network endpoint and needs validation and auth too — inputValidator handles the first; auth you add yourself (usually via middleware).
How to choose
| Next.js | TanStack Start | |
|---|---|---|
| Maturity & material | Highest; agents know it best | Newer; material growing fast |
| Server Components | Full support, the default mode | Arriving incrementally after v1 |
| Type-safe routing | Basic support | Core selling point: params, search, loaders all inferred |
| Mental model | Many conventions, more implicit behavior (caching, boundaries) | Explicit; closer to "an SPA with SSR" |
| Build tool | Turbopack | Vite (ecosystem plugins just work) |
| Deployment | Best on Vercel; elsewhere via adapters (e.g. OpenNext) | Vite-based, fairly neutral: Cloudflare, Node, Vercel, etc. |
Also worth knowing: React Router v7 (the full-stack mode after Remix merged in, loader/action style); Astro (first choice for content sites, zero JS by default with interactive islands on demand); if you later touch Vue or Svelte, their meta-frameworks are Nuxt and SvelteKit, with the same concepts as this chapter. Aware
Request waterfalls and Suspense
Server components make "just await data inside the component" very natural — which also makes request waterfalls easy: the parent awaits before rendering the child, the child awaits its own data, and total time is the sum of the layers.
Suspense marks a region that "may arrive later": the page shell streams to the browser first and shows skeletons; as each region's data becomes ready, its HTML fragment streams in over the same response and replaces the skeleton. The two Suspense regions don't wait for each other.
- Home / about page: static generation (SSG), served from the CDN.
- Bookmark list: SSR + streaming; filters in the URL; per-user data, not CDN-cached.
- Bookmark detail: a server component renders the summary (the Markdown library never reaches the browser); the delete button is a client component.
- "Ask my bookmarks" chat: mostly client components (streaming interaction, Chapter 11); the server calls the LLM.
This chapter's vocabulary: server / client components, client boundary, serializable props, hydration / hydration mismatch, streaming, Suspense boundary, request waterfall, static / dynamic rendering, revalidation.
- Pros and cons of SSR vs CSR? Compare on time-to-visible, time-to-interactive, SEO, server cost and implementation complexity.
- How do RSC and SSR differ? SSR is about "when and where to render to HTML"; RSC is about "where a component exists and whether its code ships to the browser". They're complementary.
- What is hydration, and when does it fail? The client reuses server HTML and attaches events; it fails when the first client render differs from the server's.
- What are the security risks of Server Actions? They're public endpoints needing validation, authentication and authorization; variables captured in closures may also be serialized to the client.
Self-check
1. A client component does import { db } from "@/server/db" just to use one TypeScript type exported there. What's the problem, and how should it be written?
A normal import makes the bundler include the db module (possibly with the DB driver and connection string) in browser JS, or fail the build. When you only need a type, write import type { ... }, which is fully deleted at compile time. Adding import "server-only" to server modules makes this kind of mistake surface at build time.
2. A blog post page gets updated once a day, gets heavy traffic, and needs no login. Which rendering mode would you choose, and why not SSR?
Static generation or cached rendering with timed revalidation (ISR / "use cache" with time-based invalidation). SSR would redo identical work on every request, adding server cost and latency, while content that changes once a day can easily be cached or pre-generated and served from a CDN.
3. A server component passes onDelete={() => deleteLink(id)} as a prop to a client component. What happens?
An error: plain functions aren't serializable and can't cross the server/client boundary. What can be passed is a Server Function itself (a function marked 'use server' is serialized as a reference), or just the id, letting the client component import and call the Server Function itself.