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.

⏱ ~70 minDeep (rendering modes, hydration, server/client boundary) Use (Next.js, TanStack Start)
By the end of this chapter you should be able to:
  • 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?

Opening question

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

ModeWhere HTML is producedWhenTypical use
CSR client-side renderingBrowserAfter the user opens the pageLogged-in dashboards, highly interactive tools
SSR server-side renderingServerOn every requestPersonalized pages with live data
SSG static generationBuild machineAt build (deploy) timeDocs, blogs, marketing pages (like this guide)
ISR / cached renderingServerOn first request, then cached for a whilePages whose data changes occasionally
Streaming SSRServerOn every request, sent in chunksPages 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 runsOnly on the server (or at build time)Pre-rendered once on the server + runs in the browser
Code shipped to the browser?NoYes
Can be async, read the DB, read secrets✓✗
Can use useState, useEffect, onClick✗✓
Good forData fetching, layout, static content, heavy deps (Markdown rendering, syntax highlighting)Interactivity: buttons, forms, inputs, animation
Layoutserver · reads current user LinkPageserver · await db query UserMenuclient · dropdown interaction Summaryserver · Markdown render DeleteButtonclient · onClick props: { id: "abc" } ← must be serializable Server component: code stays on server, 0 KB JS Client component: code is bundled for browser The browser downloads only the yellow parts. 'use client' is a door; every import past it ships.
A server component tree with a few client components embedded (often called "islands of interactivity"). Props that cross the boundary must be serializable: strings, numbers and plain objects are fine; functions, class instances and DB connections are not.

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).

Security essentials

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.jsTanStack Start
Maturity & materialHighest; agents know it bestNewer; material growing fast
Server ComponentsFull support, the default modeArriving incrementally after v1
Type-safe routingBasic supportCore selling point: params, search, loaders all inferred
Mental modelMany conventions, more implicit behavior (caching, boundaries)Explicit; closer to "an SPA with SSR"
Build toolTurbopackVite (ecosystem plugins just work)
DeploymentBest 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.

Where this lands in LinkNote
  • 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.
Tell the agent
Vague
Precise
"Add a delete button to the detail page."
"Keep the detail page a server component. Add a client component DeleteButton that receives only the id. Write the delete logic as a Server Action: Zod-validate the id, require login, delete only if the row belongs to the current user, and revalidate /links on success. Don't turn the whole page into a client component."
"The page has a hydration error, fix it."
"The page has a hydration mismatch; the error is below. First find which values compute differently on server vs browser (time, random numbers, window/localStorage), list them and explain the fix for each. Don't paper over it with suppressHydrationWarning."

This chapter's vocabulary: server / client components, client boundary, serializable props, hydration / hydration mismatch, streaming, Suspense boundary, request waterfall, static / dynamic rendering, revalidation.

Interview corner
  • 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.