
We know the basics from the first article: Server Components run on the server, Client Components run in the browser.
But the line between them is where most confusion lives. This article draws that line precisely.
"use client" Is a Boundary, Not a ModeThe most common misconception:
"Add
'use client'to make this component render on the client."
The reality:
"
'use client'marks the entry point to the client module graph. Everything this file imports becomes part of the client bundle."
'use client'
// ← This line is a boundary. Everything below goes to the browser.
import { useState } from 'react'
import { Button } from './button' // Button → client bundle
import { formatDate } from './utils' // formatDate → client bundle
import { heavyLibrary } from 'lib' // heavyLibrary → client bundle (even if it doesn't use state!)
export function CommentForm() {
const [text, setText] = useState('')
return <Button onClick={...}>Submit</Button>
}A single 'use client' at the top of a seemingly simple component can pull an entire dependency tree into the browser. This is why the directive should be placed as close to the interactive leaf as possible.
You can nest Server Components inside Client Components by passing them as children:
// ClientModal.tsx — Client Component (has state)
'use client'
import { useState } from 'react'
export function ClientModal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false)
return open ? <div className="modal">{children}</div> : null
}// page.tsx — Server Component (fetches data)
import { ClientModal } from './ClientModal'
import { Cart } from './Cart' // Server Component
export default function Page() {
return (
<ClientModal>
<Cart /> {/* Rendered on the server, injected as pre-rendered HTML into the Client Component slot */}
</ClientModal>
)
}Why this works: <Cart /> is rendered on the server before <ClientModal> even runs. The rendered output is passed as a prop (children) to the Client Component. The RSC Payload contains Cart's output as data — not as a component reference. Cart's code never reaches the browser.
This pattern is the backbone of Next.js component architecture: Client Components provide the shell (state, interactions); Server Components provide the content (data, rendering).
Synchronous props — direct data transfer:
// page.tsx (Server)
export default async function Page() {
const post = await getPost(1)
return <LikeButton likes={post.likes} /> // Pass a number
}
// LikeButton.tsx (Client)
'use client'
export function LikeButton({ likes }: { likes: number }) {
const [count, setCount] = useState(likes)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}Props must be serializable — numbers, strings, booleans, arrays, plain objects. No functions, no Promises, no class instances.
Streaming data — pass a Promise and resolve it on the client with use():
// page.tsx (Server)
export default function Dashboard() {
const statsPromise = getStats() // Fire, don't await
return (
<Suspense fallback={<Loading />}>
<StatsChart dataPromise={statsPromise} />
</Suspense>
)
}
// StatsChart.tsx (Client)
'use client'
import { use } from 'react'
export function StatsChart({ dataPromise }) {
const stats = use(dataPromise) // Suspends until resolved
return <div>{stats.revenue}</div>
}This is useful when you want the static shell to render immediately while waiting for data.
React Context doesn't work in Server Components. The standard pattern is a Client Component wrapper:
// ThemeProvider.tsx
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext('light')
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('dark')
return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
}
// layout.tsx (Server)
import { ThemeProvider } from './ThemeProvider'
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}Wrap providers as tightly as possible — notice <ThemeProvider> wraps only {children}, not the entire <html>. This lets Next.js optimize the static HTML shell.
Modules can be imported in both server and client contexts. To prevent server-only code (database queries, API keys) from leaking into the client:
// lib/data.ts
import 'server-only' // Build-time error if imported from a Client Component
export async function getSecretData() {
return db.query('SELECT * FROM secrets')
}Similarly, client-only guards code that uses browser APIs (window, localStorage) from being imported on the server.
Next.js also strips non-
NEXT_PUBLIC_environment variables from the client bundle automatically.
Does this component need:
useState / useEffect / onClick / event handlers?
→ Client Component ('use client')
Direct database access / API keys / async data fetching?
→ Server Component (default)
Both? (data fetching + interactivity)
→ Split into two:
1. Server Component fetches data
2. Client Component handles interactions
3. Nest using the interleaving pattern (children)'use client' is a module graph boundary, not a rendering mode.children — Server Components rendered before the Client Component receives them.server-only and client-only prevent accidental cross-environment imports.Previous:
Next: