React Server Components 07 🧬 Client Boundaries — The “use client” directive and module graphs
The "use client" directive is not a label that says “this component is interactive.” It is a boundary marker in the module dependency tree. When a file contains the directive at its top, that module and every module it imports—transitively—become part of the client module graph. The framework stops server-rendering at that boundary, ships the marked code to the browser, and lets the client finish the render .
This chapter covers how the directive defines the boundary, what crosses it (serializable props and JSX children), what cannot cross it (plain functions and event handlers), and why a component definition can be both a Server Component and a Client Component depending on where it is imported. It also covers the placement rule that keeps the client bundle small: push the directive as low in the component tree as possible.
Key point: "use client" marks a module boundary, not a render tree node. A parent-child relationship in the render tree does not guarantee the same render environment. A Client Component can render a Server Component if the Server Component was rendered by a server module and passed as children.
Why the directive is a boundary, not a label
The module graph problem. React Server Components are evaluated by walking the module dependency tree, not the render tree. When the framework encounters a module with "use client", it stops server-evaluating that subtree. Everything below that point—the file itself and every module it imports—is bundled for the client . A component defined in that subtree is a Client Component by virtue of where it lives in the module graph, not because someone labeled it.
The dual-identity problem. The same component definition can be a Server Component in one usage and a Client Component in another. The React documentation uses FancyText as the example: it has no directive, so when imported by a server module (App.js) it is a Server Component. When the same FancyText is imported by a module with "use client" (InspirationGenerator.js), that usage becomes a Client Component, and the component’s code is downloaded to the browser . The definition is one function; the usage determines the environment.
The children escape hatch. A Client Component can render a Server Component without importing it. The server module renders the Server Component and passes the resulting JSX as children to the Client Component. The Client Component receives serialized React elements, not the Server Component’s code. The Server Component’s implementation never enters the client bundle . This is why the module graph, not the render tree, is the unit of analysis.
a. Placing the directive
The directive must be the first statement in the file, before any imports. Comments above it are allowed. It must use single or double quotes, not backticks . It applies to the entire module and cannot be placed inside a component or function.
// app/components/counter.tsx
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
Placing it at the top of a file makes every export from that file a client entry point. You do not need to repeat the directive in every file that contains Client Components. You only need it at the entry point—the file whose components are imported directly by a Server Component .
If a shared component is used by both server and client code and should remain a Server Component when possible, do not add the directive to it. Instead, create a Client Component wrapper that imports the shared component and place the directive on the wrapper. This keeps the shared module out of the client bundle when it is used by server code .
b. What crosses the boundary: serializable props and JSX
Props passed from a Server Component to a Client Component must be serializable. The supported types include primitives, plain objects, arrays, Date, Map, Set, Promise, React elements (JSX), and Server Functions marked with "use server" .
// Server Component passing serializable data
import { Counter } from './counter'
export default function Page() {
return <Counter initialCount={5} label="Clicks" />
}
Plain functions do not cross. An event handler like onClick cannot be passed as a prop from a Server Component to a Client Component. The Client Component must define its own handler internally, or receive a Server Function as a reference .
// Wrong: onClick is a plain function, not serializable
<Counter onClick={() => console.log('clicked')} /> // ❌
// Correct: the Client Component defines its own handler
// Counter receives only serializable data
JSX children are serializable because they are rendered React elements. This is what allows a Server Component to nest inside a Client Component without importing the Server Component’s code into the client graph .
// Server Component (no directive)
import { Modal } from './modal' // Client Component
import { Cart } from './cart' // Server Component
export default function Page() {
return (
<Modal title="Cart">
<Cart /> {/* Server Component passed as children */}
</Modal>
)
}
Modal is a Client Component. It receives title (a string) and children (serialized React elements). It renders the children in the positions defined by its implementation. The Cart component runs on the server; Modal never sees its code, only its output .
c. When to add the directive
Server Components are the default. Reach for "use client" only when the component needs something the server cannot provide: state (useState, useReducer), effects (useEffect, useLayoutEffect), event handlers (onClick, onChange), browser APIs (window, localStorage, canvas), or third-party libraries that use client-only features like createContext or forwardRef .
The goal is to push the directive as low in the component tree as possible. If only a button needs useState, the directive belongs on the button, not on the page that contains it. The page stays a Server Component and the client bundle contains only the button’s code .
// page.tsx (Server Component)
import { LikeButton } from './like-button'
export default async function PostPage({ id }: { id: string }) {
const post = await getPost(id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton postId={id} /> {/* Only this is client */}
</article>
)
}
// like-button.tsx (Client Component)
'use client'
import { useState } from 'react'
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false)
return <button onClick={() => setLiked(!liked)}>{liked ? 'Liked' : 'Like'}</button>
}
The page’s data fetching and static content run on the server. Only the button’s interactive code is sent to the browser.
Complete Example Session
// ============================================
// PART 1: THE DIRECTIVE AT THE TOP
// ============================================
// app/components/editor.tsx
'use client'
import { useState } from 'react'
import { formatDate } from './formatters' // becomes client code
export function Editor({ timestamp }: { timestamp: number }) {
const [text, setText] = useState('')
return (
<div>
<p>{formatDate(timestamp)}</p>
<textarea value={text} onChange={(e) => setText(e.target.value)} />
</div>
)
}
// Both Editor and formatDate are client modules.
// The directive applies to the entire module subtree.
// ============================================
// PART 2: A SERVER COMPONENT IMPORTS THE CLIENT COMPONENT
// ============================================
// app/page.tsx (no directive)
import { Editor } from './components/editor'
export default function Page() {
return <Editor timestamp={Date.now()} />
}
// Page is a Server Component.
// Editor is a Client Component entry point.
// ============================================
// PART 3: PASSING SERIALIZABLE PROPS
// ============================================
// Server Component
<Editor timestamp={1234567890} title="Note" />
// timestamp: number (serializable)
// title: string (serializable)
// Both cross the boundary successfully.
// ============================================
// PART 4: A FUNCTION PROP FAILS
// ============================================
// Server Component
<Editor onSave={() => console.log('saved')} /> // ❌
// onSave is a plain function.
// Functions cannot cross from server to client.
// React throws an error at runtime.
// ============================================
// PART 5: PASSING A SERVER FUNCTION
// ============================================
// app/actions.ts
'use server'
export async function saveNote(text: string) {
await db.note.create({ data: { text } })
}
// Server Component
<Editor onSave={saveNote} /> // ✅
// saveNote is a Server Function.
// It crosses as a reference, not as code.
// ============================================
// PART 6: PASSING A SERVER COMPONENT AS CHILDREN
// ============================================
// app/components/modal.tsx
'use client'
export function Modal({ children }: { children: React.ReactNode }) {
return <div className="modal">{children}</div>
}
// app/page.tsx (Server Component)
import { Modal } from './components/modal'
import { Cart } from './cart' // Server Component
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
)
}
// Cart is rendered on the server.
// Modal receives the rendered output as children.
// Cart's code never enters the client bundle.
// ============================================
// PART 7: PUSHING THE DIRECTIVE DOWN
// ============================================
// Wrong: the whole page is a Client Component
'use client'
export default async function Page() { /* ... */ } // ❌
// Correct: only the interactive part is client
// page.tsx (Server Component)
import { LikeButton } from './like-button'
export default async function Page() {
const data = await fetchData()
return <div>{data.title}<LikeButton /></div>
}
// ============================================
// PART 8: SHARED COMPONENT WRAPPER
// ============================================
// shared/button.tsx (no directive, can be server or client)
export function Button({ children }: { children: React.ReactNode }) {
return <button className="btn">{children}</button>
}
// client-wrapper.tsx
'use client'
import { Button } from './shared/button'
export function ClientButton() {
return <Button onClick={() => alert('clicked')}>Click</Button>
}
// Button is shared.
// ClientButton is the client entry point.
// ============================================
// PART 9: THE MODULE GRAPH DECISION
// ============================================
// Does the file need useState, useEffect, or onClick?
// Yes → add "use client" at the top
// No → leave it without a directive
//
// Is a shared component used by both server and client?
// Yes → keep it without a directive, wrap it for client use
// ============================================
// PART 10: THE COMPLETE BOUNDARY
// ============================================
// Server Component (no directive) → can import Client Component
// Client Component ("use client") → can import other Client Components
// Client Component → cannot import a Server Component directly
// Client Component → can receive a Server Component as children
The ten parts covered the directive placement, server importing client, serializable props, the function-prop failure, Server Function references, children as an escape hatch, pushing the directive down, the shared wrapper pattern, the decision tree, and the complete boundary rules.
Quick Reference
Directive rules
| Rule | Detail |
|---|---|
| Placement | First statement, before imports |
| Quotes | Single or double, not backticks |
| Scope | Entire module and transitive imports |
| Effect when imported by client | None (already client) |
| Required in every client file | No, only at the entry point |
What crosses the boundary
| Type | Crosses |
|---|---|
| Primitives (string, number, boolean) | Yes |
| Plain objects, arrays | Yes |
| Date, Map, Set, Promise | Yes |
| React elements (JSX) | Yes |
Server Functions ("use server") | Yes (as reference) |
| Plain functions (event handlers) | No |
| Class instances | No |
When to use
| Need | Directive |
|---|---|
useState, useReducer | "use client" |
useEffect, useLayoutEffect | "use client" |
onClick, onChange | "use client" |
window, localStorage | "use client" |
createContext, forwardRef | "use client" |
| Data fetching, DB access | None (Server Component) |
| Static content | None (Server Component) |
Boundary patterns
| Pattern | Result |
|---|---|
| Server imports Client | Client Component entry point |
| Client imports Client | No effect, already client |
| Client imports Server | Error |
| Server passes JSX children to Client | Server Component renders on server |
| Server passes plain function to Client | Error |
Best Practices
✅ Do This:
// Place the directive at the top, before imports
'use client'
import { useState } from 'react' // ✅
// Push the directive as low as possible
<LikeButton /> // only the button is client // ✅
// Pass Server Components as children to Client Components
<Modal><Cart /></Modal> // ✅
// Pass Server Functions as references
<Form action={createPost} /> // ✅
❌ Don’t Do This:
// Add the directive to every file that has a client feature
// Only the entry point needs it // ❌
// Pass an event handler from a Server Component
<Button onClick={() => {}} /> // ❌ (not serializable)
// Import a Server Component directly into a Client Component
'use client'
import { ServerComp } from './server' // ❌ (becomes client)
// Add "use client" to a shared component used by server code
// Wrap it for client use instead // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Function prop throws | Plain functions are not serializable | Use a Server Function or define the handler in the Client Component |
| Server Component becomes client code | Imported by a "use client" module | Pass as children instead of importing |
| Directive has no effect | Imported by another client module | Expected; directive only matters at the server-client entry |
| Client bundle too large | Directive placed too high in the tree | Push it down to the smallest interactive unit |
| Shared component breaks in client | useState used in a file without directive | Wrap it in a Client Component or move the state to a client file |
| Compound component error | Static member crosses the boundary | Use named exports instead of static properties |
Real-World Examples
1. Basic client entry point
'use client'
import { useState } from 'react'
2. Client Component with serializable props
'use client'
export function Chart({ data }: { data: number[] }) { ... }
3. Server Component passing children
<ClientWrapper>
<ServerContent />
</ClientWrapper>
4. Server Function as a prop
'use server'
export async function deleteItem(id: string) { ... }
// <DeleteButton action={deleteItem} />
5. Pushing the directive down
// page.tsx (Server)
<article>{content}<LikeButton /></article>
6. Shared component wrapper
'use client'
import { SharedButton } from './shared'
export function ClientButton() { return <SharedButton /> }
7. Conditional client feature
// If no client feature is needed, no directive
export function StaticCard({ title }: { title: string }) { ... }
8. Non-serializable error
// <Button onClick={() => {}} /> // Error
9. Client importing client
// No directive needed; already in client graph
10. Server Component inside Client Component
// Client Component receives Server Component as children
<Modal>{serverRenderedContent}</Modal>
Visual
The module dependency boundary
┌─────────────────────────────────────────────────────────────┐
│ MODULE GRAPH │
│ │
│ App.js (no directive) │
│ │ │
│ ├── FancyText.js (no directive) │
│ │ → Server Component │
│ │ │
│ └── InspirationGenerator.js ('use client') │
│ │ │
│ ├── FancyText.js (same file) │
│ │ → Client Component (this usage) │
│ │ │
│ └── inspirations.js (transitive dependency) │
│ → Client module │
│ │
│ The boundary is on the module tree, not the render tree. │
└─────────────────────────────────────────────────────────────┘
Children escape hatch
┌─────────────────────────────────────────────────────────────┐
│ CLIENT COMPONENT RENDERS SERVER COMPONENT │
│ │
│ page.tsx (Server) │
│ │ │
│ ├── imports Modal ('use client') │
│ │ │
│ └── imports Cart (no directive) │
│ │ │
│ ▼ │
│ <Modal title="Cart"> │
│ <Cart /> ← rendered on server, passed as children │
│ </Modal> │
│ │
│ Modal receives serialized JSX, not Cart's code. │
│ Cart's implementation stays on the server. │
└─────────────────────────────────────────────────────────────┘
Serializable vs non-serializable props
┌─────────────────────────────────────────────────────────────┐
│ CROSSES DOES NOT CROSS │
│ │
│ string, number, boolean plain function │
│ plain object, array class instance │
│ Date, Map, Set, Promise event handler │
│ JSX element non-global Symbol │
│ Server Function (reference) │
└─────────────────────────────────────────────────────────────┘
Push the directive down
┌─────────────────────────────────────────────────────────────┐
│ CLIENT BUNDLE SIZE │
│ │
│ WRONG: directive at page level │
│ page.tsx ('use client') │
│ → entire page + all imports sent to browser │
│ │
│ RIGHT: directive at button level │
│ page.tsx (Server Component) │
│ → page rendered on server │
│ like-button.tsx ('use client') │
│ → only button code sent to browser │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Directive | "use client" |
| Placement | Top of file, before imports |
| Scope | Module and all transitive imports |
| Boundary type | Module dependency tree, not render tree |
| What crosses | Serializable props, JSX children, Server Functions |
| What does not cross | Plain functions, class instances |
| Entry point rule | Only the file imported by a Server Component needs the directive |
| Shared components | Keep without directive; wrap for client use |
| Server Component as children | Allowed; rendered on server, passed as JSX |
Key takeaways:
"use client"is a boundary in the module graph, not a label on a component. The directive marks a file and everything it imports as client code. The framework server-renders until it hits that boundary, then ships the marked subtree to the browser .- The same component definition can be both a Server Component and a Client Component. The environment is determined by the module that imports and uses it, not by the file that defines it. A component without a directive becomes a Client Component when imported by a client module .
- Only serializable data crosses the boundary. Primitives, plain objects, arrays, Date, JSX, and Server Functions pass. Plain functions and event handlers do not. The Client Component must define its own handlers or receive a Server Function reference .
- Children are the escape hatch. A Client Component can render a Server Component if the Server Component is rendered by a server module and passed as
children. The Client Component receives the rendered JSX, not the Server Component’s code . - Push the directive as low as possible. The client bundle contains everything below the boundary. If only a button needs state, the directive belongs on the button, not the page .
- Shared components should stay without a directive. If a component is used by both server and client code, keep it directive-free and wrap it in a Client Component for the client use case. This keeps the shared module out of the client bundle when server code uses it .
Remember: The "use client" directive is not a declaration that a component is interactive. It is a marker that starts a client module subtree. Everything below it is bundled and sent to the browser. Everything above it remains server code. The boundary is on the module dependency tree, not the render tree, which is why a Client Component can render a Server Component passed as children. Place the directive at the smallest unit that needs client features, pass serializable data across the boundary, and use children to nest Server Components inside Client Components without importing their code.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!