Where the Client Boundary Sits: The Module Graph, Not the JSX Tree
Your dashboard page renders a <RevenueChart> that calls useState, and the dev server stops with You're importing a component that needs "useState". This React Hook only works in a Client Component.... So you add 'use client' to the page. The error clears — and now every import that page pulls in ships to the browser as JavaScript. The directive solved the error while moving the boundary one level higher up the tree than you intended. Nothing about the page changed except which modules the bundler now treats as client code — the server-rendered default you learned in Server Components by Default still governs every module no directive has opted out.
The directive is a marker on the module graph, not a switch on the JSX tree. Next.js describes it as declaring "a boundary between the Server and Client module graphs." When a file begins with 'use client', the bundler treats that module plus every module it transitively imports as client code: those files are bundled for the browser, executed during hydration, and re-executed on every client-side render. Nothing below the directive in the JSX output matters, and nothing above it in JSX makes you safe.
Placement rules are strict and worth memorizing:
- It must be the first statement of the file, above the imports. Comments before it are allowed.
- It must use single or double quotes. Backticks are rejected.
- It only means something when server code imports the file. When another client module imports it, the directive changes nothing.
'use client'; // line 1, before imports
import { useState } from 'react';
import { formatMoney } from '@/lib/format'; // now part of the client graph
import { Button } from './Button'; // client too, directive not required
export function AddToCart({ sku, price }) {
const [added, setAdded] = useState(false);
// ...
}
| Question | Answer |
|---|---|
| What becomes client code? | The file with the directive and everything it imports transitively |
| What stays server-side? | Modules never imported from the client graph, including JSX passed as children |
| Does it disable server rendering? | No — client components are still SSR'd for the initial HTML |
| Where must it go? | Line 1, above imports, single or double quotes |
Two subtleties separate a passing answer from a great one in interviews. First: 'use client' does not mean "runs only in the browser." Client components render on the server for the initial HTML pass too, which is why touching window during render still blows up during prerender with ReferenceError: window is not defined. The directive means the module is also downloaded and run in the browser, so browser-only APIs belong inside useEffect.
Second, because the boundary cuts the import tree rather than the render tree, one definition can execute in both environments. React's docs use a FancyText component defined without the directive: imported by a server page it is a Server Component usage and only its rendered output crosses the wire; imported and rendered inside a client InspirationGenerator, the same function becomes a Client Component usage and its source ships to the browser. Component usage, not component definition, decides which side you are on.
That same fact explains why a client component can render server components it never imports: you hand them over as JSX children. The server creates the JSX and renders it before the client component starts, so the client receives finished output rather than server source code. The full mechanics are in Crossing the Client Boundary.
Three Errors That Reveal the Boundary Is Missing or in the Wrong Place
Next.js fails loudly when server code needs a client runtime. Learning to read the three common messages by sight turns a debugging session into a ten-second fix.
Error 1 — a hook import. When a file in the server module graph calls a client-only hook, compilation stops with a message naming the hook:
You're importing a component that needs "useState".
This React Hook only works in a Client Component but none of its parents
are marked with "use client", so they're Server Components by default.
Wordings vary slightly across releases (Next.js 15 shortens it to This React Hook only works in a Client Component), but the shape is stable: hook name, then "Client Component." The fix is to add the directive to the component's own file, not to the page. Marking the page fixes the symptom and drags the whole page's import graph into the bundle — the mistake the previous chapter priced out.
Error 2 — an event handler crossing the boundary. If server code passes a function into anything React has to serialize for the client, you get:
Error: Event handlers cannot be passed to Client Component props.
<button onClick={function} children=...>
^^^^^^^^^^^^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.
Read that carefully: the message blames "Client Component props," but the offender is almost always server-side code — a page.tsx without the directive rendering <button onClick={...}>, or a server page passing onClick={handler} into a client <Button>. React maintainers have fielded complaints about this wording (issue #61058); the rule underneath is simple: functions defined in server code cannot reach the client, with one exception — a Server Function marked 'use server'. The fixes are either moving the handler into a client component or passing data down and letting the client define its own callback.
Error 3 — metadata on a marked file. If you put 'use client' on a page or layout that also exports metadata, the build reports:
You are attempting to export "metadata" from a component marked with
"use client", which is disallowed. Either remove the export, or the
"use client" directive.
Metadata must be resolved on the server before the page renders, so the segment file has to stay server-side. Extract the interactive subtree into its own file with the directive and import it.
Equally important is what does not require the directive. Server Components can await, and they may use useId, useMemo, useCallback, and React's cache() — none of them depend on state, effects, or browser APIs. Also delete a common mix-up from your vocabulary: there is no directive for Server Components. 'use server' marks Server Functions (callable endpoints), not server components.
Third-party libraries are the last trap. A package that uses hooks without shipping its own directive compiles fine — Next.js skips its hook check inside node_modules — and then fails during prerender with a TypeError naming the hook. The documented fix is a two-line wrapper:
'use client';
export { Carousel } from 'acme-carousel';
Next.js also honors the server-only and client-only marker packages so importing a secret-holding module from client code fails with a clear message instead of leaking.
Timing matters when you triage these. Statically rendered routes surface them during next build's prerender step, so CI catches them before users do; dynamic routes throw on the first request instead. If an error only shows up in production, check whether the route is dynamic — the boundary rule is identical, only the moment you meet it moves. A quick audit after the fix: grep the client chunks in .next/static/chunks for a string that exists only in your server-only file. Finding it there means the directive, or something it imports transitively, is still pulling server code into the browser.
Choosing the Leaf: What Every Directive Costs Your Bundle
The performance question in an interview is rarely "what does 'use client' do" — it is "where do you put it, and how do you know you got it right?" The official guidance is one sentence: move Client Components to the leaves of your component tree where possible.
Consider a product page. The page itself needs to query the database, so it stays a Server Component. Only the add-to-cart button needs state and click handling:
// app/products/[sku]/page.tsx — Server Component, no directive
import db from '@/lib/db';
import AddToCartButton from './AddToCartButton';
export default async function ProductPage({ params }) {
const product = await db.product.findUnique({ where: { sku: params.sku } });
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton sku={product.sku} price={product.price} />
</main>
);
}
// AddToCartButton.tsx — the leaf island
'use client';
import { useState } from 'react';
export default function AddToCartButton({ sku, price }) {
const [quantity, setQuantity] = useState(1);
// ...
}
Everything above the button — the database call, the markdown renderer, the review list — executes on the server and never enters the browser bundle. Move the directive up to page.tsx and all of it does.
The cost is measurable. Run next build with the directive on the leaf, note the route's size table (the First Load JS column), then move it to the page and build again. In a typical app the difference is not a few kilobytes: the page-level directive drags every route-local import into the client chunk, often doubling or tripling what the browser must download and parse before the page becomes interactive. The bundle number is your argument — quote it when an interviewer asks how you would justify keeping a component server-side.
The bytes are only half the tax. Once a segment file carries the directive you cannot await data in it — async components are not supported on the client — so fetching migrates into useEffect, which runs after hydration. The user now waits for the HTML, then the JS, then the request: latency you removed by keeping the page on the server.
Two honest counterweights show depth. First, there are cases where you force a component to be a client component on purpose: React's docs note that when a component's output HTML is much larger than its source — a component returning a very long inline SVG path, say — shipping the source can be cheaper than re-serializing its markup on every server render. Second, a client component at the root is sometimes structural rather than lazy: a theme or auth provider must call createContext, which has no server equivalent, so the provider file is client code while the server children it wraps are not.
Before adding the directive, run this quick audit:
| Ask | If the answer is yes |
|---|---|
| Does it use state, effects, refs, or event handlers? | It needs the directive — put it in its own file |
| Does it only render markup from props? | Leave it server-side; its output crosses as HTML |
| Is it a third-party component with hooks? | Wrap it in a 'use client' re-export file |
| Are you adding it to a page to silence an error? | Stop — move the directive down to the failing leaf |
| Does the file also export server utilities or secrets? | Keep those in server modules and guard with server-only |
When you are done, the shape you want is a mostly-server tree with a handful of small interactive islands — that is the design the directive exists to make possible, and the placement discipline that Crossing the Client Boundary builds on.
Quiz
1. Where must the 'use client' directive appear to take effect?
2. A page without 'use client' calls useState and the build stops. What message do you see?
3. What does adding 'use client' to one file actually mark as client code?
4. Which component can stay a Server Component without the directive?
Flashcards
Question
What exactly does the 'use client' directive mark, and where must it be placed?
Click to reveal answer
Answer
It marks the file and all of its transitively imported modules as client code (the client module graph). It must be the first statement in the file, above the imports, written with single or double quotes — never backticks — and it has no effect when one client module imports another.
Question
Does 'use client' mean the component only runs in the browser?
Click to reveal answer
Answer
No. Client components are still server-rendered for the initial HTML, then executed in the browser during hydration. That is why window or document access must live inside useEffect — touching them during render fails prerendering with 'ReferenceError: window is not defined' even in a client component.
Question
What is the difference between a component definition and a component usage at the server/client boundary?
Click to reveal answer
Answer
The boundary applies to usages. A component defined without 'use client' is a Server Component usage when imported by server code (only its output crosses), but becomes a Client Component usage when rendered inside a client module — so the same function can be evaluated on the server and shipped to the browser in the same app.
Question
Which hooks and features work in a Server Component without 'use client'?
Click to reveal answer
Answer
useId, useMemo, useCallback, React's cache(), plus async/await and direct data access. Stateful or effect hooks (useState, useReducer, useEffect, useRef, useContext) require a client module. There is no 'use server' directive for server components — 'use server' marks Server Functions.
Question
You must add interactivity to a shared component that has no directive. What are the two sanctioned fixes?
Click to reveal answer
Answer
Either create a small wrapper file that starts with 'use client' and re-exports the component (the standard fix for third-party libraries, whose node_modules code skips the hook check and dies at prerender with a TypeError), or split your own component so only the interactive leaf carries the directive and the container stays server-side.
Revision Notes
Key Takeaways
- 1. 'use client' marks the module and its transitive imports as client code — the boundary is on the module graph, never on the JSX tree
- 2. Client components still server-render for initial HTML; browser APIs must stay inside useEffect or prerender throws 'ReferenceError: window is not defined'
- 3. Component usage decides the side: the same definition can be a server usage in one place and a client usage in another
- 4. Hooks, event handlers, context creation, and browser APIs require the directive; async rendering, useId, useMemo, useCallback and cache() do not
- 5. There is no directive for Server Components — 'use server' marks Server Functions only, and metadata exports are disallowed in client-marked files
Interview Tips
- • "Explain what 'use client' does" — answer in three clauses: it marks the module plus its transitive imports as client code, it must sit on line 1 above the imports, and it does not disable server rendering; the module still SSRs for initial HTML
- • "Your page's JavaScript doubled after a fix. What happened?" — someone added 'use client' high in the tree to silence the hook error; show how you would move it to the failing leaf and re-measure with next build
- • "Can a Server Component render a Client Component and vice versa?" — yes both ways: server renders client children as islands, and client components receive server JSX as children because the server renders it first
- • "What would you pass from a server page to a client button?" — serializable data (ids, labels, counts), never a callback; if the server must trigger behavior, pass a Server Function marked 'use server'
Cheat Sheet
The 'use client' Directive
Placement:
'use client'; // line 1 — above imports, ' or " quotes only
import { useState } from 'react';
What it marks: the file and its transitive imports = the client module graph. JSX position is irrelevant; server children passed in from outside stay server components.
What forces the directive: useState / useReducer / useEffect / useRef / useContext, createContext, event handlers, forwardRef / memo / startTransition, browser APIs, third-party libs using any of these.
What does NOT: await data fetching, useId, useMemo, useCallback, cache(), plain markup-from-props components.
Error → Fix:
| Error | Meaning | Fix |
|---|---|---|
This React Hook only works in a Client Component |
hook called in server graph | directive on the component's own file |
Event handlers cannot be passed to Client Component props. |
function crosses from server code | define handler client-side, or pass a 'use server' action |
You are attempting to export "metadata" from a component marked with "use client" |
segment file over-marked | keep page/layout server, extract the interactive part |
ssr: false is not allowed with next/dynamic in Server Components |
dynamic opt-out outside client code | move dynamic() into a client wrapper |
Cost rule: every module under the directive ships. Compare next build route sizes with the directive on a leaf versus on the page — that number is your justification for leaf placement.