Next.js 05 ▲ Server and Client Components — Rendering boundaries & composition patterns
The App Router is built on a split that did not exist in the previous React versions: the Server Components and the Client Components. Every component in the app/ directory is a Server Component by default, and it runs only on the server. The 'use client' directive marks the boundary where the component code becomes part of the client bundle and runs in the browser as well as on the server . The previous chapters covered the layouts and the navigation; this chapter explains the rendering model that determines what code ships to the browser, what data can cross the boundary, and how to compose the two kinds of components without breaking the boundary.
The distinction is not about the server-side rendering. A Client Component is still rendered to HTML on the server during the initial load and during the static generation; the difference is that its JavaScript is also sent to the browser, where React hydrates it and makes it interactive . A Server Component’s code never reaches the browser. It can read the database, the filesystem, and the secrets, and it sends only the rendered output, the RSC Payload, to the client . The boundary is the mechanism that decides which code is in which graph, and the composition patterns are the mechanism that lets the server-rendered UI nest inside the client-interactive UI without pulling the server code into the client bundle.
Key point: The 'use client' directive defines a boundary in the module graph, not in the render tree. Once a file is marked, all of its imports and the components it directly renders become Client Components, but a Server Component passed as a children prop or another prop is not imported into the client graph; it is rendered on the server and passed to the Client Component as the already-rendered output . The props that cross the boundary must be serializable: the functions, the classes, and the Dates cannot be passed directly, while the primitives, the arrays, the plain objects, and the React elements (JSX) can .
Why Server and Client Components require the boundary
The bundle-size problem.
The client JavaScript bundle is the cost of the interactivity. Every component that uses the useState, the useEffect, the event handlers, or the browser APIs must ship its code to the browser. A component that only renders the static content does not need to. The Server Components are the default because they keep the code on the server and send only the HTML and the RSC Payload, which reduces the bundle size and the hydration cost . The 'use client' directive should be pushed as deep into the tree as possible, so that the static parts stay on the server and only the interactive islands ship to the client.
The data-access problem.
A Server Component can read the database, the filesystem, and the environment secrets directly, without an API route. The data fetching happens during the render, and the result is included in the RSC Payload . A Client Component cannot do this, because its code runs in the browser and the browser cannot read the server’s filesystem or the private environment variables. The boundary is the mechanism that decides which component can access which resource.
The context problem.
The React context is not supported in the Server Components, because the context relies on the client-side state and the provider. To use the context, a Client Component must be created that accepts the children and renders the provider, and the Server Component can then render that provider and pass the rest of the tree as its children . The provider should be rendered as deep as possible, so that the static parts of the tree remain Server Components.
a. The Server Components and the data access
A Server Component is the default. It can be an async function, and it can await the data directly.
// app/posts/page.tsx (Server Component by default)
import { db } from '@/lib/db'
export default async function PostsPage() {
const posts = await db.query('SELECT * FROM posts')
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
The PostsPage is an async function, and it awaits the database query during the render . The db module and the query never reach the browser, because the Server Component’s code is not included in the client bundle. The rendered output, the list of the post titles, is serialized into the RSC Payload and sent to the client.
The Server Component can also pass the data to a Client Component as props, but the props must be serializable.
// app/posts/page.tsx
import LikeButton from './like-button'
export default async function PostsPage() {
const post = await getPost(1)
return (
<div>
<h1>{post.title}</h1>
<LikeButton likes={post.likes} />
</div>
)
}
The likes is a number, which is serializable, so it crosses the boundary correctly. The post object itself could be passed if it were a plain serializable object, but the functions and the class instances cannot cross . The LikeButton is a Client Component, and it receives the likes prop and manages the interactive state.
b. The Client Components and the interactivity
A Client Component is marked with the 'use client' directive at the top of the file, before the imports.
// app/posts/like-button.tsx
'use client'
import { useState } from 'react'
export default function LikeButton({ likes }: { likes: number }) {
const [count, setCount] = useState(likes)
return (
<button onClick={() => setCount(count + 1)}>
❤️ {count}
</button>
)
}
The 'use client' directive tells Next.js that the LikeButton and its imports are part of the client module graph . The useState and the onClick are the client features that require the directive. The component is rendered on the server during the initial load, and hydrated in the browser to attach the event handler.
The 'use client' directive does not need to be repeated in every component that the Client Component imports. Once the boundary is declared, all of the imports and the directly rendered components are part of the client graph.
// app/posts/like-button.tsx
'use client'
import { useState } from 'react'
import { formatCount } from './utils' // This is now a Client Component too
export default function LikeButton({ likes }: { likes: number }) {
const [count, setCount] = useState(likes)
return <button>{formatCount(count)}</button>
}
The formatCount is imported into the Client Component, so its module is part of the client bundle . The utils.ts file does not need the 'use client' directive, because the boundary is already declared by the file that imports it.
c. The composition patterns and the serialization
The composition is the mechanism that lets the Server Components nest inside the Client Components without pulling the server code into the client bundle.
// app/ui/modal.tsx (Client Component)
'use client'
import { useState } from 'react'
export default function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(!open)}>Open</button>
{open && <div className="modal">{children}</div>}
</div>
)
}
The Modal is a Client Component, and it manages the open state. The children is the prop that receives the server-rendered content.
// app/page.tsx (Server Component)
import Modal from './ui/modal'
import Cart from './ui/cart'
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
)
}
The Cart is a Server Component, and it is passed as the children of the Modal . The Cart is rendered on the server, and its output is included in the RSC Payload. The Modal receives the rendered output, not the Cart module, so the Cart code never reaches the client . This is the pattern that allows the interactive chrome to wrap the static content without making the content interactive.
The serialization is the constraint on the props that cross the boundary.
// ✅ Serializable props
<ClientComponent
name="Alice" // string
age={30} // number
active={true} // boolean
items={['a', 'b']} // array
meta={{ key: 'value' }} // plain object
icon={<ServerIcon />} // React element (JSX)
/>
// ❌ Not serializable
<ClientComponent
onClick={() => {}} // function
date={new Date()} // class instance (use ISO string)
user={new User()} // class instance
/>
The primitives, the arrays, the plain objects, and the React elements are serializable . The functions, the classes, and the Dates are not. The Date should be passed as an ISO string and parsed on the client, and the function should be replaced by a Server Action if the client needs to invoke the server logic .
Complete Example Session
// ============================================
// PART 1: THE SERVER COMPONENT (DEFAULT)
// ============================================
// app/products/page.tsx
import { db } from '@/lib/db'
export default async function ProductsPage() {
const products = await db.query('SELECT * FROM products')
return (
<div>
<h1>Products</h1>
<ProductList products={products} />
</div>
)
}
// ============================================
// PART 2: THE CLIENT COMPONENT
// ============================================
// app/products/product-list.tsx
'use client'
import { useState } from 'react'
export default function ProductList({ products }: { products: Product[] }) {
const [filter, setFilter] = useState('')
const filtered = products.filter((p) =>
p.name.toLowerCase().includes(filter.toLowerCase())
)
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Search"
/>
<ul>
{filtered.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</div>
)
}
// ============================================
// PART 3: THE SERVER COMPONENT AS CHILDREN
// ============================================
// app/ui/modal.tsx (Client)
'use client'
import { useState } from 'react'
export default function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(!open)}>Toggle</button>
{open && <div>{children}</div>}
</div>
)
}
// app/page.tsx (Server)
import Modal from './ui/modal'
import Cart from './ui/cart'
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
)
}
// ============================================
// PART 4: THE CONTEXT PROVIDER
// ============================================
// app/ui/theme-provider.tsx (Client)
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext('light')
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
// app/layout.tsx (Server)
import ThemeProvider from './ui/theme-provider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
The four parts demonstrate the boundary: the Server Component with the database access, the Client Component with the interactive filter, the Server Component passed as the children of the Client Component, and the context provider as the Client Component that wraps the Server Component children.
Quick Reference
Server vs Client
| Feature | Server Component | Client Component |
|---|---|---|
| Directive | None (default) | 'use client' |
| Runs on server | Yes | Yes (SSR/SSG) |
| Runs in browser | No | Yes (hydration) |
| Async/await | Yes | No |
useState/useEffect | No | Yes |
| Event handlers | No | Yes |
| Browser APIs | No | Yes |
| Direct DB/secrets | Yes | No |
| Bundle size | Zero client JS | Included in bundle |
Serialization Rules
| Prop Type | Serializable |
|---|---|
| String, number, boolean | ✅ |
| Array, plain object | ✅ |
| React element (JSX) | ✅ |
| Date | ❌ (use ISO string) |
| Function | ❌ (use Server Action) |
| Class instance | ❌ |
Composition Patterns
| Pattern | Purpose |
|---|---|
Client wraps Server children | Interactive chrome around static content |
| Server passes Client as child | Static page with interactive island |
Client provider wraps Server children | Context for the whole tree |
'use client' deep in tree | Minimize bundle size |
Best Practices
✅ Do This:
// Start with the Server Components
export default async function Page() { ... } # ✅
// Add the 'use client' only where the interactivity is needed
'use client' # ✅
// Pass the Server Components as the children
<ClientWrapper><ServerComponent /></ClientWrapper> # ✅
// Keep the client leaves small
<Search /> // only the search is client # ✅
// Pass the serializable data only
<Client likes={post.likes} /> # ✅
// Render the providers deep in the tree
<ThemeProvider>{children}</ThemeProvider> # ✅
❌ Don’t Do This:
// Add the 'use client' to the static components
'use client' // unnecessary for the static content # ❌
// Import the Server Component into the Client Component
import { ServerComp } from './server' // Becomes client # ❌
// Pass the functions as the props
<Client onClick={() => {}} /> // Not serializable # ❌
// Use the context in the Server Component
useContext(ThemeContext) // Not supported # ❌
// Mark the whole page as client for one interactive element
'use client' // The entire page ships to the client # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The whole page is client-side | 'use client' at the top | Move it to the interactive leaf |
| The Server Component becomes client | Imported into a Client Component | Pass it as children |
| The serialization error | A function prop | Use a Server Action or remove it |
| The context is undefined | Used in a Server Component | Create a Client provider |
| The hydration mismatch | The different output on server and client | Check the conditional rendering |
Real-World Examples
1. Server Component with data
export default async function Page() {
const data = await db.query(...)
return <List data={data} />
}
2. Client Component with state
'use client'
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
3. Client wraps Server children
<Modal><Cart /></Modal>
4. Server passes Client as child
<Page><Counter /></Page>
5. Context provider
'use client'
export default function ThemeProvider({ children }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
6. Serializable props
<Client name="Alice" age={30} items={['a']} />
7. Non-serializable prop
<Client onClick={() => {}} /> // Error
8. The server-only package
import 'server-only'
export async function getSecret() { ... }
9. The third-party wrapper
'use client'
import { Carousel } from 'acme-carousel'
export default function ClientCarousel(props) { return <Carousel {...props} /> }
10. The 'use client' at the leaf
// app/ui/search.tsx
'use client'
export default function Search() { ... }
Visual
The Module Graph Boundary
┌──────────────────────────────────────────────────────────────┐
│ THE BOUNDARY IS IN THE MODULE GRAPH, NOT THE RENDER TREE │
│ │
│ Server module graph: │
│ ├── app/page.tsx (Server) │
│ ├── app/ui/cart.tsx (Server) │
│ └── app/ui/modal.tsx (Client) ← the boundary │
│ │
│ Client module graph: │
│ ├── app/ui/modal.tsx (Client) │
│ └── (its imports) │
│ │
│ The Cart is rendered on the server and passed as children │
│ to the Modal. The Cart module is not in the client graph. │
└──────────────────────────────────────────────────────────────┘
The RSC Payload
┌──────────────────────────────────────────────────────────────┐
│ THE RSC PAYLOAD CONTAINS THE RENDERED SERVER OUTPUT │
│ │
│ On the server: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Server Component tree → RSC Payload │ │
│ │ ├── the rendered result of the Server Components │ │
│ │ ├── the placeholders for the Client Components │ │
│ │ └── the serialized props for the Client Components │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ On the client: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ HTML (the initial preview) │ │
│ │ + RSC Payload (reconcile the trees) │ │
│ │ + JS (hydrate the Client Components) │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
The Composition
┌──────────────────────────────────────────────────────────────┐
│ THE CLIENT WRAPS THE SERVER CHILDREN │
│ │
│ <Modal> ← Client Component │
│ <Cart /> ← Server Component (children) │
│ </Modal> │
│ │
│ The Modal manages the open state and the click handler. │
│ The Cart reads the database and renders the items. │
│ The Cart code never reaches the client. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Default component | Server Component |
| Client directive | 'use client' at the top of the file |
| Server Component code | Never ships to the browser |
| Client Component code | Ships and hydrates in the browser |
| Async components | Server only |
useState/useEffect | Client only |
| Serialization | Primitives, arrays, objects, JSX |
| Non-serializable | Functions, classes, Dates |
| Server as children | Passed as props, stays server-rendered |
| Context | Client provider wrapping the children |
Key takeaways:
- The Server Components are the default. Every component in the
app/directory is a Server Component unless the'use client'directive is added. The Server Components run only on the server, and their code never reaches the browser . - The
'use client'directive defines the module graph boundary. Once a file is marked, all of its imports and the components it directly renders are part of the client bundle. The boundary is not in the render tree; a Server Component passed as a prop is not imported into the client graph . - The props that cross the boundary must be serializable. The primitives, the arrays, the plain objects, and the React elements are serializable. The functions, the classes, and the Dates are not. The Date should be passed as an ISO string, and the function should be replaced by a Server Action .
- The composition lets the Server Components nest inside the Client Components. The Client Component wraps the
children, and the Server Component is passed as thechildren. The Server Component is rendered on the server, and its output is passed to the Client Component without the server code reaching the client . - The context requires a Client provider. The React context is not supported in the Server Components. The Client Component that accepts the
childrenand renders the provider is the mechanism that allows the whole tree to consume the context . - The
'use client'should be pushed deep. The static parts stay on the server, and only the interactive islands ship to the client. The entire page should not be marked as client for one interactive element .
Remember: The Server and Client Components are the two halves of the App Router. The Server Components are the default and the data access layer. The Client Components are the interactivity layer, and the 'use client' directive is the boundary. The composition patterns let the two nest without breaking the boundary, and the serialization rules determine what can cross. The result is the minimal client bundle with the full server access, where the static content is rendered on the server and the interactive islands are hydrated in the browser.
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!