Server Components vs Client Components
'use client' decides which code ships to the browser, not where a component renders, and every prop crossing it is public.
Predict first
Before you read: predict
ExamHall exam page, production build (next build, then next start). app/exam/logs/page.jsx has no directive:
TimerBadge.jsx starts with 'use client'. TimerBadge's body calls console.log('render TimerBadge') and returns <p>{minutes}:00 left</p>.
A candidate opens the page once in Chromium.
Which lines appear in the server's terminal, and which in the browser's console?
import { connection } from 'next/server';import TimerBadge from '../../../components/exam/TimerBadge'; export default async function ExamPage() { await connection(); // rendered for each request console.log('render ExamPage'); return <main><h1>Physics paper 2</h1><TimerBadge minutes={40} /></main>;}The 30-second version
Server Components run only on the server: they may be async, read secrets and ship no code. 'use client' starts a module-graph boundary: that file and all it imports ship to the browser. Client Components render on the server, then again in the browser to hydrate. Props crossing the boundary are serialised into the RSC payload, readable by anyone.
'use client' decides which code ships to the browser, not where a component renders, and every prop crossing it is public.
How it works
Step 1 of 10
'use client'.Analogy: A toy company's packing line (toy manufacturing)
The factory builds some toys completely and ships them finished: no parts, no instructions, none of its tools. Other orders go out as kits in a crate stamped "kit", and everything packed into that crate travels, stamped or not. The factory also assembles every kit once for the shop window; at home the buyer follows the booklet against that model to make it move. The packing list taped to a crate is read by whoever receives it.
| At the packing line | In Next |
|---|---|
| a toy shipped finished, with no tools | a Server Component: output only (step 2) |
| the crate stamped "kit" and all it holds | the 'use client' file and its imports (step 3) |
| the window model, then the booklet at home | server render, then hydration (step 4) |
| the packing list on the crate | props in the RSC payload (steps 5, 7) |
| a finished figure riding in a kit's display slot | server output passed as children (step 8) |
| a price book stamped "factory floor only", which the line refuses to crate | server-only (step 9) |
Breaks down when: a crate holds physical things, while a prop holds only data: you cannot pack a function (step 6). Wrong inference: "anything a component can use can be passed to it."
Contrasting case: a factory tour. Visitors watch the machines run but take none home, as a browser sees Server Component output without its code.
In real terms: the server graph produces the RSC payload and references to client modules; the client graph, bundled from each 'use client' entry, renders again in the browser.
Examples, run for real
Every yes/no it prints is asserted.
4.1 ExamHall EXH-1500: the answer key in the payload
The proctor console's live feed flagged a candidate answering every question correctly within seconds. The exam page, a Server Component, loaded each question row and passed it whole to AnswerCard ('use client'), which shows only the stem and the options. MR !1700 passes a view object built on the server.
const row = await getQuestion('q7'); // { id, stem, options, correctOption, explanation }
return <AnswerCard question={row} />; // MR !1700: question={{ id: row.id, stem: row.stem, options: row.options }}
4.1 whole row (EXH-1500) four options on screen yes · correctOption in the page's inline RSC payload yes · in the RSC response yes · explanation in the payload yes · in .next/static no
4.1 view object (MR !1700) four options on screen yes · correctOption in the page's inline RSC payload no · in the RSC response no · explanation in the payload no · in .next/static no
The screen looked right both times. The key travelled anyway, inside the payload that ships with the page (steps 5, 7). Only the view object keeps it on the server.
4.2 Catalog: a product page that holds a pricing key
ProductPage is an async Server Component. It reads process.env.PRICING_API_KEY, awaits the price service, and renders <AddToCart sku="TB-42" price={price} />, a Client Component with a basket counter.
4.2 product page · price 129 on screen yes · in the RSC payload yes · AddToCart code in .next/static yes
4.2 pricing key · in the HTML no · in the RSC response no · in .next/static no
The key stays on the server: the component that read it never ships (step 2). The price crosses because it is a prop (step 5), and AddToCart's code ships because it is a Client Component (step 3).
Try it
Answer first, then check. Nothing is saved.
Question 1 of 6
A production build of an exam page. page.jsx (no directive) imports QuestionText from QuestionText.jsx (no directive) and AnswerEditor from AnswerEditor.jsx, which starts with 'use client'. AnswerEditor.jsx imports RubricHint from RubricHint.jsx (no directive) and wordCount from lib/word-count.js (no directive), and uses both.
Whose code is in the JavaScript the browser downloads?
Question 2 of 6
The exam page, a Server Component rendered for each request, renders <TimerProp minutes={40} extra={…} />, and TimerProp starts with 'use client'. Four versions differ only in the value of extra.
Which version fails when the page renders?
Question 3 of 6
A practice-mode question page, a Server Component, renders <Reveal><WorkedSolution /></Reveal>. WorkedSolution has no directive and returns the worked solution's text. Reveal starts with 'use client', keeps shown (initially false) and returns shown ? children : <button>Show worked solution</button>.
Before the candidate presses the button, where is the solution's text in what the browser has received?
Question 4 of 6
lib/answer-key.js exports async function getAnswerKey(questionId), which returns a question's correct option. A teammate imported it into a 'use client' component. The team wants any future import of this module from client code to fail next build.
Which change to lib/answer-key.js does that?
Question 5 of 6
A boot's product page renders SizeGuideDrawer, which starts with 'use client', keeps an open state, imports StockBySize and renders it once the shopper opens the drawer. StockBySize (no directive) passes process.env.WAREHOUSE_TOKEN to the warehouse client and lists the sizes in stock. In the browser, the opened drawer says "Stock unavailable".
Which change makes the opened drawer list the sizes, with the token never reaching the browser?
Question 6 of 6
A product page, page.jsx (no directive), shows its Markdown description inside DescriptionTabs, a 'use client' component with a tab state. The team adds a Markdown library and wants none of the library's code in the browser's JavaScript, with the description still shown in its tab.
Where should renderMarkdown be called?
Go deeper
Pro tips
- Before a review, search the page's own response for a field that should stay secret. Open view-source (or fetch the page) and search for the field name, such as
correctOption; the inline RSC payload is in theself.__next_fscripts.- When it helps: any Server Component that passes a database row to a Client Component (steps 5, 7).
- Status: RAN (Next 16.3.6, Node 24.19.0).
- Read the import trace in a boundary build error from the bottom up. Next prints the chain from the page down to the offending module, each line tagged with its graph (
[Server Component],[Client Component Browser]); the first tag change is where the boundary sits.- When it helps: a
server-onlyor "must be async" build error in a large tree (steps 3, 9). - Verify:
builds/guard-server-only.jsonholds such a trace. - Status: RAN (
build.mjs, recorded once).
- When it helps: a
Source
Next.js pages were fetched as the Markdown nextjs.org serves on Accept: text/markdown.
- Next.js docs, Server and Client Components (docs version 16.3.7): https://nextjs.org/docs/app/getting-started/server-and-client-components ·
next-server-client-components.txt. ""use client"is used to declare a boundary between the Server and Client module graphs"; "all of its imports and the components it directly renders are included in the client bundle"; the RSC payload contains "Any props passed from a Server Component to a Client Component"; "Props passed to Client Components need to be serializable by React"; Server Components passed aschildren"are rendered on the server ahead of time"; "If variables are not prefixed, Next.js replaces them with an empty string" (step 10 keeps the docs' wording as a note; itsundefinedis executed, see the next bullet);server-onlygives "a build-time error"; "React context is not supported in Server Components". Named in the curriculum row. Valid as of 2026-09-30. - Next.js docs, The Server and Client Boundary (docs version 16.3.6): https://nextjs.org/docs/app/guides/server-and-client-boundary ·
next-server-client-boundary.txt. The server/browser table ("Client Component · Yes · Yes"); "Its log appears in the terminal and the browser console"; "A Server Component's code never reaches the browser"; "Passing a function as a prop from a Server Component to a Client Component throws"; "Modalonly ever sees its output, never its code". Supports steps 2, 4, 6, 8. - Next.js docs, Data Security: https://nextjs.org/docs/app/guides/data-security ·
next-data-security.txt. Background for step 7 and MR !1700 (return only the data the UI needs). Named in the curriculum row. - React,
'use client': https://react.dev/reference/rsc/use-client ·react-use-client.txt. The directive marks the module and its transitive dependencies as client code; serialisable prop types. Supports steps 3, 6. Named in the curriculum row. - React, Server Components and
'use server': https://react.dev/reference/rsc/server-components · https://react.dev/reference/rsc/use-server ·react-server-components.txt,react-use-server.txt. Async components; "Server Components cannot create context, but they can render a context provider imported from a Client Component module";'use server'marks server functions that client code can call. - Also saved, not quoted:
next-environment-variables.txt. - Re-run 2026-10-15, exit 0:
plain-name: client component sends String(process.env.DRM_KEY_ONE) -> the string undefined (the value was undefined) · marker set at build and at start.
Related lessons
Checked against the tools and docs as of 2026-09-30.