| |

React 90 ⚛️ Server Components vs Client Components Rules

⏱️ Estimated read: 12 min | |

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/await in 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 useId in some configurations, though the general rule is that hooks are not available

Not allowed:

  • useState, useReducer, useEffect, useLayoutEffect, useContext
  • Event handlers like onClick or onChange
  • 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, and useContext
  • Event handlers like onClick and onChange
  • Browser APIs like window, document, and localStorage
  • 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 children instead)
'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

CapabilityServer ComponentClient Component
async/awaitYesNo
useState, useEffectNoYes
Event handlersNoYes
Browser APIsNoYes
Database accessYesNo
Ships to clientNoYes
DirectiveNone'use client'

Directive Rules

RuleDetail
PlacementTop of file, before imports
ScopeModule and all transitive imports
No directiveServer Component by default
'use server'Not for components; for Server Functions

Boundary Composition

PatternAllowed
Server imports ClientYes
Client imports ServerNo (pass as children)
Server passes Client as childrenYes
Client passes Server as childrenYes (from Server parent)

Serializable Props

TypeSerializable
PrimitivesYes
Plain objects, arraysYes
DateYes
React elements (JSX)Yes
FunctionsNo
Class instancesNo

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

PitfallWhy It HappensFix
Whole page ships to client'use client' placed too highMove to the interactive leaf
Function prop throwsProps must be serializableMove handler into Client Component
Server Component not foundImported in a Client ComponentPass as children from Server parent
Context unavailable in ServerContext is client-onlyWrap Client Components in a provider
Third-party component failsLibrary uses client featuresWrap 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

ItemValue
Default componentServer Component
Client marker'use client'
Boundary locationModule graph
Server hooksNone (except useId in some cases)
Client hooksAll
Server data accessDirect
Client browser APIsYes
Props across boundaryMust be serializable
Client importing ServerNot allowed
Server as childrenAllowed

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 children because 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.


Was this helpful?


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!