React 90 ⚛️ Server Components vs Client Components Rules
The rules that separate Server Components from Client Components are not stylistic guidelines. They are enforced by the compiler, the bundler, and the React runtime, and violating them produces build errors or serialization exceptions. The core rule is deceptively simple: a component is a Server Component by default, and it becomes a Client Component only when it is defined in a module marked with 'use client' or imported transitively from such a module . The boundary lives on the module dependency tree, not on the render tree. A component can be rendered as a Server Component in one place and as a Client Component in another, depending on which module imported it .
The constraints that follow from this rule shape how RSC applications are architected. Server Components cannot use state, effects, event handlers, or browser APIs, because their code never reaches the browser . Client Components can use all of those, but they pay for it by shipping their code to the client. The boundary between them is a serialization boundary: props passed from a Server Component to a Client Component must be serializable, and the serialized data is visible in the RSC Payload . There is no 'use server' directive for Server Components—that directive is for Server Functions, a separate feature .
Key point: Server Components are the default and cannot use interactive APIs or browser APIs. Client Components are marked with 'use client', run on both server and client, and can use state, effects, and event handlers. The 'use client' boundary is on the module graph, not the render tree, so a Server Component can be passed as children to a Client Component and still execute entirely on the server before the Client Component renders.
Why the rules are enforced this way
The environment split problem. A Server Component runs in an environment that has no window, no document, and no browser event system. Allowing a Server Component to call useState would produce code that cannot execute, because the state hook depends on the React client runtime. The rule that Server Components cannot use hooks is not a limitation of the feature—it is a direct consequence of where the code runs .
The serialization boundary problem. When a Server Component renders a Client Component, the props cross from the server environment to the client environment. React serializes them into the RSC Payload, and the Client Component deserializes them. Functions, class instances, and symbols cannot be serialized, which is why passing an onClick handler from a Server Component to a Client Component throws an exception . The rule forces the developer to keep interactive logic on the client side of the boundary.
The module graph problem. The 'use client' directive marks a module and everything it imports as client code. If a page module contains the directive, the entire page and its dependency subtree ship to the browser. If the directive is placed on a small interactive leaf, only that leaf and its dependencies ship. The rule about placement is a rule about bundle size, and it is the single most important performance decision in an RSC application .
The composition problem. A Client Component cannot import a Server Component directly. The client module graph does not have access to the server module graph. But a Client Component can receive a Server Component as children or as a prop, because the server renders the Server Component first and passes its already-rendered output through the RSC Payload . This is the pattern that lets a client-side modal contain server-fetched content without making the content client-side.
a. The rules for Server Components
A Server Component is any component that is not part of a client module subtree. It can be an async function, and it can access server-only resources during its render.
Allowed:
async/awaitin the component body- Direct database queries, filesystem reads, and internal service calls
- Passing serializable data and JSX as props to Client Components
- Rendering Client Components as children
- Using
useIdin some configurations, though the general rule is that hooks are not available
Not allowed:
useState,useReducer,useEffect,useLayoutEffect,useContext- Event handlers like
onClickoronChange - Browser APIs like
window,document,localStorage - Importing a module that uses client-only features without wrapping it
// A Server Component: async, no hooks, direct data access
export default async function ProductPage({ params }) {
// Direct database query during render
const product = await db.products.findFirst({ where: { id: params.id } })
return (
<main>
<h1>{product.name}</h1>
{/* Passing serializable data to a Client Component */}
<AddToCartButton productId={product.id} />
</main>
)
}
The AddToCartButton is a Client Component, and the productId is a primitive that can cross the boundary. If the Server Component tried to pass a function as a prop, the render would throw .
b. The rules for Client Components
A Client Component is any component defined in a module marked with 'use client' or imported transitively from such a module. It runs on the server during SSR and again in the browser during hydration .
Allowed:
- All React hooks, including
useState,useEffect, anduseContext - Event handlers like
onClickandonChange - Browser APIs like
window,document, andlocalStorage - Third-party libraries that depend on client-side features
Not allowed:
- Direct database or filesystem access (that is a Server Component capability)
- Importing a Server Component directly (use
childreninstead)
'use client'
import { useState } from 'react'
export function AddToCartButton({ productId }) {
// State is allowed because this is a Client Component
const [added, setAdded] = useState(false)
return (
<button onClick={() => setAdded(true)}>
{added ? 'Added' : 'Add to cart'}
</button>
)
}
The 'use client' directive must be at the very top of the file, before any imports or code. Comments are allowed above it, but nothing else . The directive applies to the entire module and everything it imports, so placing it on a large module pulls the entire dependency subtree into the client bundle .
c. The boundary rules and composition patterns
The most important rule about the boundary is that it lives on the module graph, not the render tree. A Client Component can render a Server Component if the Server Component is passed to it as children from a parent Server Component .
// A Client Component that renders whatever children it receives
'use client'
export function Modal({ children }) {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(true)}>Open</button>
{open && <div className="modal">{children}</div>}
</div>
)
}
// A Server Component that passes a Server Component as children
import { Modal } from './Modal'
import { Cart } from './Cart'
export default function Page() {
return (
<Modal>
{/* Cart is a Server Component, but it is rendered on the server
before Modal starts rendering on the client */}
<Cart />
</Modal>
)
}
The Cart component is a Server Component even though it appears inside the Client Component Modal. The module graph decides: Page imports Cart from a server module, so Cart is a Server Component. Modal never imports Cart; it receives it as children . The Server Component executes fully before the Client Component begins its client render.
The serialization rule applies to every prop that crosses the boundary. Primitives, plain objects, arrays, dates, and React elements are serializable. Functions, class instances, and symbols are not . The RSC Payload contains the serialized props, so anything passed across the boundary is visible to anyone inspecting network traffic . A database secret passed as a prop would be exposed; a server-only module import would fail at build time if the client code tried to import it .
Complete Example Session
The following session demonstrates the boundary rules in a page with a Server Component, a Client Component, and a Server Component passed as children.
// ============================================
// PART 1: THE SERVER COMPONENT PAGE
// ============================================
// app/page.js
import { getProducts } from './data'
import { FilterBar } from './FilterBar'
import { ProductList } from './ProductList'
export default async function Page() {
// Async render with direct data access
const products = await getProducts()
return (
<main>
{/* FilterBar is a Client Component; it receives serializable data */}
<FilterBar categories={products.map(p => p.category)} />
{/* ProductList is a Server Component; it renders on the server */}
<ProductList products={products} />
</main>
)
}
// ============================================
// PART 2: THE CLIENT COMPONENT
// ============================================
// app/FilterBar.js
'use client'
import { useState } from 'react'
export function FilterBar({ categories }) {
// State is allowed here
const [selected, setSelected] = useState('all')
return (
<select value={selected} onChange={(e) => setSelected(e.target.value)}>
<option value="all">All</option>
{categories.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
)
}
// ============================================
// PART 3: THE SERVER COMPONENT CHILD
// ============================================
// app/ProductList.js
export function ProductList({ products }) {
// No hooks, no state, no event handlers
return (
<ul>
{products.map((p) => <li key={p.id}>{p.name}</li>)}
</ul>
)
}
// ============================================
// PART 4: PASSING A SERVER COMPONENT AS CHILDREN
// ============================================
// app/CartModal.js
'use client'
import { useState } from 'react'
export function CartModal({ children }) {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(true)}>Cart</button>
{open && <div>{children}</div>}
</div>
)
}
// ============================================
// PART 5: THE PARENT THAT COMPOSES THEM
// ============================================
// app/layout.js
import { CartModal } from './CartModal'
import { Cart } from './Cart'
export default function Layout({ children }) {
return (
<CartModal>
{/* Cart is a Server Component passed as children */}
<Cart />
{children}
</CartModal>
)
}
The five parts cover the Server Component page, the Client Component with state, the Server Component child, the Client Component that accepts children, and the composition in the layout. What the session demonstrates is that the boundary is determined by the module graph, and a Server Component can be rendered inside a Client Component if it is passed as children from a Server parent.
Quick Reference
Component Capabilities
| Capability | Server Component | Client Component |
|---|---|---|
async/await | Yes | No |
useState, useEffect | No | Yes |
| Event handlers | No | Yes |
| Browser APIs | No | Yes |
| Database access | Yes | No |
| Ships to client | No | Yes |
| Directive | None | 'use client' |
Directive Rules
| Rule | Detail |
|---|---|
| Placement | Top of file, before imports |
| Scope | Module and all transitive imports |
| No directive | Server Component by default |
'use server' | Not for components; for Server Functions |
Boundary Composition
| Pattern | Allowed |
|---|---|
| Server imports Client | Yes |
| Client imports Server | No (pass as children) |
Server passes Client as children | Yes |
Client passes Server as children | Yes (from Server parent) |
Serializable Props
| Type | Serializable |
|---|---|
| Primitives | Yes |
| Plain objects, arrays | Yes |
| Date | Yes |
| React elements (JSX) | Yes |
| Functions | No |
| Class instances | No |
Best Practices
✅ Do This:
// Place 'use client' on the smallest interactive leaf
'use client'
export function LikeButton() { /* ... */ }
// Pass Server Components as children to Client Components
<ClientModal><ServerCart /></ClientModal>
// Keep secrets in Server Components
const secret = process.env.API_KEY // never passed as prop
❌ Don’t Do This:
// Place 'use client' on the page module
'use client' // entire page ships to client
// Pass a function across the boundary
<ClientButton onClick={() => {}} /> // throws
// Import a Server Component inside a Client Component
import { Cart } from './Cart' // in a 'use client' file
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Whole page ships to client | 'use client' placed too high | Move to the interactive leaf |
| Function prop throws | Props must be serializable | Move handler into Client Component |
| Server Component not found | Imported in a Client Component | Pass as children from Server parent |
| Context unavailable in Server | Context is client-only | Wrap Client Components in a provider |
| Third-party component fails | Library uses client features | Wrap in a 'use client' module |
Real-World Examples
1. Server Component with async data
export default async function Page() {
const data = await fetchData()
return <List data={data} />
}
2. Client Component with state
'use client'
export function Counter() { const [n, setN] = useState(0); /* ... */ }
3. Pass Server Component as children
<ClientModal><ServerContent /></ClientModal>
4. Serializable prop
<ClientComp items={['a', 'b']} />
5. Non-serializable prop
<ClientComp onSelect={() => {}} /> // throws
6. Client Component importing Client Component
'use client'
import { Button } from './Button' // Button is also client
7. Server Component importing Client Component
import { Button } from './Button' // Button has 'use client'
8. Context provider in a Client Component
'use client'
export function ThemeProvider({ children }) { return <Ctx.Provider>{children}</Ctx.Provider> }
9. Server Component rendering the provider
<ThemeProvider><ClientWidget /></ThemeProvider>
10. Third-party component wrapper
'use client'
export function CarouselWrapper(props) { return <Carousel {...props} /> }
Visual
Module Graph Boundary
┌──────────────────────────────────────────────┐
│ SERVER MODULE GRAPH │
│ Page.js (Server Component) │
│ │ │
│ ├── imports → data.js (server-only) │
│ ├── imports → ProductList.js (Server) │
│ └── imports → FilterBar.js │
│ │ 'use client' │
│ ▼ │
│ CLIENT MODULE GRAPH │
│ FilterBar.js (Client Component) │
│ └── imports → useState (client runtime) │
└──────────────────────────────────────────────┘
Passing Server as Children
┌──────────────────────────────────────────────┐
│ SERVER RENDER │
│ Page renders <CartModal><Cart /></CartModal>│
│ │ │
│ ▼ │
│ Cart (Server) renders first │
│ its output becomes part of the RSC Payload │
│ │ │
│ ▼ │
│ CLIENT RENDER │
│ CartModal receives children (Cart output) │
│ and renders it without importing Cart │
└──────────────────────────────────────────────┘
Serializable Boundary
┌──────────────────────────────────────────────┐
│ Server Component │
│ props = { id: 1, name: "Alice" } │
│ │ │
│ ▼ │
│ RSC Payload serializes props │
│ │ │
│ ▼ │
│ Client Component │
│ receives { id: 1, name: "Alice" } │
│ │
│ Functions, classes, symbols → throw │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Default component | Server Component |
| Client marker | 'use client' |
| Boundary location | Module graph |
| Server hooks | None (except useId in some cases) |
| Client hooks | All |
| Server data access | Direct |
| Client browser APIs | Yes |
| Props across boundary | Must be serializable |
| Client importing Server | Not allowed |
| Server as children | Allowed |
Key takeaways:
- Server Components are the default. A component is a Server Component unless it is part of a
'use client'module subtree . - The boundary is on the module graph, not the render tree. A Client Component can render a Server Component passed as
childrenbecause the Server Component was imported by a Server parent . - Server Components cannot use state, effects, event handlers, or browser APIs. Their code never reaches the browser, so those features have no runtime .
- Client Components can use everything but pay in bundle size. The
'use client'directive should be placed as deep as possible in the module graph . - Props crossing the boundary must be serializable. Functions, class instances, and symbols throw an exception .
- There is no
'use server'directive for Server Components. That directive is for Server Functions . - Third-party components that use client features must be wrapped. Add
'use client'to your own wrapper module .
Remember: The rules for Server and Client Components are not conventions—they are enforced by the bundler and the runtime. A Server Component is code that never leaves the server, so it cannot use features that require a browser. A Client Component is code that runs in both places, so it can use everything but ships to the client. The 'use client' directive is the boundary, and the module graph is where it lives. The serialization rule is the hard edge: what crosses the boundary must be data, not behavior. When the compiler rejects a function prop or a hook in a Server Component, it is enforcing the architecture, not being difficult.
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!