Server and Client Components
The idea in one sentence
Section titled “The idea in one sentence”In the App Router every component is a Server Component until a file says "use client", and that one line draws a boundary that decides what code ever reaches the browser.
Server Components are the default
Section titled “Server Components are the default”A component in app/ runs on the server unless you opt out. That means it can be async, await data, and touch the database, the filesystem, or secrets directly — none of that code is sent to the browser.
import { db } from '@/lib/db';
export default async function Dashboard() { const users = await db.user.findMany(); // runs on the server only return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;}Because this never ships to the client, the browser downloads zero JavaScript for it. Server Components cannot use useState, useEffect, event handlers, or browser APIs — they render once, on the server.
”use client” marks a Client Component
Section titled “”use client” marks a Client Component”Put "use client" at the very top of a file and everything it exports becomes a Client Component: it runs in the browser and may use hooks, state, effects, event handlers, and browser APIs.
'use client';
import { useState } from 'react';
export default function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;}The subtle part: the boundary is transitive. Every module a Client Component imports is pulled into the client bundle too, whether or not it has its own directive. You mark the boundary once, at the top of the tree; you do not repeat "use client" in every child.
Composing across the boundary
Section titled “Composing across the boundary”Two rules govern how the two kinds mix, and they are not symmetric:
- A Server Component can import and render a Client Component directly.
- A Client Component cannot import a Server Component — but it can accept one as
childrenor a prop.
That second rule is the doughnut (slot) pattern: the Client Component is the ring, and a Server Component fills the hole. The server-rendered content stays on the server even though a client component surrounds it.
'use client';export default function ClientShell({ children }: { children: React.ReactNode }) { return <div className="shell">{children}</div>;}export default function ServerData() { return <p>Rendered on the server</p>;}import ClientShell from './client-shell';import ServerData from './server-data';
export default function Page() { return ( <ClientShell> <ServerData /> {/* stays a Server Component, passed as children */} </ClientShell> );}One constraint makes this work: props crossing the boundary must be serializable. You can pass strings, numbers, Dates, plain objects, arrays, and JSX, but not functions (other than Server Actions) or class instances as raw props — they cannot survive the trip from server to client. The next two sections show exactly what happens when you try.
Streaming a Promise into a Client Component with use()
Section titled “Streaming a Promise into a Client Component with use()”Instead of await-ing data in the Server Component and passing the resolved value down, you can start the fetch and hand the unresolved Promise straight to a Client Component. The Client Component unwraps it with React’s use(), and the nearest <Suspense> boundary shows a fallback until it resolves. The static shell streams immediately; the slow data arrives later, with no client-side useEffect fetch involved.
export async function getNotifications(userId: string) { await new Promise((r) => setTimeout(r, 20)); return [{ id: '1', text: `Hello ${userId}` }, { id: '2', text: 'Another one' }];}'use client'import { use } from 'react'
export function NotificationList({ notificationsPromise,}: { notificationsPromise: Promise<{ id: string; text: string }[]>}) { // Unwraps the Promise on the client — suspends the nearest boundary until it resolves const notifications = use(notificationsPromise) return ( <ul> {notifications.map((n) => ( <li key={n.id}>{n.text}</li> ))} </ul> )}import { Suspense } from 'react'import { NotificationList } from './notification-list'import { getNotifications } from '@/lib/notifications-data'
export default function NotificationsPage() { // Do not await — hand the unresolved Promise straight to the Client Component const notificationsPromise = getNotifications('u_1') return ( <Suspense fallback={<p>Loading notifications…</p>}> <NotificationList notificationsPromise={notificationsPromise} /> </Suspense> )}Built in the probe (next build), this compiles and prerenders cleanly — use() is exactly what lets a Promise, which is not itself serializable JSON, cross the boundary: React handles the Promise specially and streams its resolution, rather than trying to serialize it as a plain value.
Marking modules server-only or client-only
Section titled “Marking modules server-only or client-only”A plain module with no directive can be imported from either side, and nothing stops a teammate (or a generated snippet) from importing a server-secrets file into a Client Component by mistake — it will not throw at the type level, and only leaks at runtime. The server-only package (and its counterpart client-only) turns that mistake into a build-time error instead:
import 'server-only'
export function getSecret() { return process.env.SECRET_KEY ?? 'unset'}'use client'import { getSecret } from '@/lib/secret-data'
export default function Bad() { return <p>{getSecret()}</p>}tsc --noEmit accepts both files — there is nothing type-level wrong here. Running next build in the probe is where it actually fails, with two real errors:
Error: You're importing a module that depends on "server-only". This API isonly available in Server Components in the App Router, but you are using itin the Pages Router. Learn more: https://nextjs.org/docs/app/building-your-application/rendering/server-components
Error: 'server-only' cannot be imported from a Client Component moduleIt should only be used from a Server Component.(The first message’s “Pages Router” wording is boilerplate reused across both routers — the second message is the one that actually names the problem.) Installing server-only/client-only is optional in Next.js — the framework recognizes both package names internally and produces this error whether or not the packages are actually installed from npm — but installing them keeps a linter that flags missing dependencies quiet, and gives editors a real module to resolve.
What crosses the boundary — and what throws
Section titled “What crosses the boundary — and what throws”Serializable data crosses; functions and class instances do not. Both failures are real, provable errors — not type errors, but errors React throws while prerendering:
'use client'export default function ClientReceiver({ onSave }: { onSave: () => void }) { return <button onClick={onSave}>Save</button>}import ClientReceiver from './client-receiver'
function saveOnServer() { console.log('saved')}
export default function Page() { return <ClientReceiver onSave={saveOnServer} />}tsc --noEmit passes — () => void is a perfectly ordinary function type. next build fails during prerendering:
Error: Event handlers cannot be passed to Client Component props. {onSave: function d} ^^^^^^^^^^If you need interactivity, consider converting part of this to a Client Component.A class instance fails the same way, for a different reason — React only serializes plain objects and a short list of built-ins across the boundary:
'use client'class UserModel { constructor(public name: string) {}}export default function ClientReceiver({ user }: { user: UserModel }) { return <p>{user.name}</p>}import ClientReceiver from './client-receiver-user'
class UserModel { constructor(public name: string) {}}
export default function Page() { const user = new UserModel('Ada') return <ClientReceiver user={user} />}Error: Only plain objects, and a few built-ins, can be passed to ClientComponents from Server Components. Classes or null prototypes are notsupported. {user: {name: "Ada"}} ^^^^^^^^^^^^^The one exception to “no functions”: a Server Function ('use server') crosses as a reference, not as a closure — that is what makes passing an action to a form or button work at all, and it is covered in What Are Server Actions.
Keep the client boundary low
Section titled “Keep the client boundary low”Because the boundary is transitive, where you put "use client" decides how much JavaScript ships — not just for the interactive part, but for every purely-presentational component that happens to sit under it. Two versions of the same layout make the difference concrete: one marks the whole layout as a Client Component, the other keeps the layout on the server and pushes "use client" down to a small Counter leaf.
// Static presentational content — no interactivity, no client-only APIs.export function Sidebar() { return <aside data-widget="sidebar">Recent posts, tags, links.</aside>}
export function Nav() { return <nav data-widget="nav">Home · Blog · About · Contact</nav>}
export function Footer() { return <footer data-widget="footer">© Example Co. All rights reserved.</footer>}'use client'// 'use client' on the layout — everything it imports and renders directly// joins the client bundle, including Nav/Sidebar/Footer, which have no// interactivity of their own.import { useState } from 'react'import { Sidebar, Nav, Footer } from '@/lib/widgets'
export default function HighLayout({ children }: { children: React.ReactNode }) { const [count, setCount] = useState(0) return ( <div> <Nav /> <Sidebar /> <button onClick={() => setCount((c) => c + 1)}>{count}</button> {children} <Footer /> </div> )}// A plain Server Component layout — Nav/Sidebar/Footer never leave the server.import { Sidebar, Nav, Footer } from '@/lib/widgets'import Counter from './counter'
export default function LowLayout({ children }: { children: React.ReactNode }) { return ( <div> <Nav /> <Sidebar /> <Counter /> {children} <Footer /> </div> )}'use client'// 'use client' pushed down to only the interactive leaf.import { useState } from 'react'
export default function Counter() { const [count, setCount] = useState(0) return <button onClick={() => setCount((c) => c + 1)}>{count}</button>}Nav, Sidebar, and Footer are ordinary presentational components with no hooks — in low, they stay Server Components and ship no JavaScript at all. next build no longer prints a per-route bundle-size column (removed in v16.0.0), so the real proof comes from reading the emitted client-reference manifest and summing the referenced chunk files under .next/static/chunks/ (next experimental-analyze --output shows the same data as an interactive treemap):
--- /heavy/high --- chunk 3fntmmi971322.js 14377 bytes (shared framework chunk — both routes load this) chunk 3gejn2apugqil.js 8967 bytes (route-specific: Nav + Sidebar + Footer + Counter)TOTAL referenced by /heavy/high: 23344 bytes
--- /heavy/low --- chunk 3fntmmi971322.js 14377 bytes (same shared framework chunk) chunk 1p8pi2r0mnrj-.js 287 bytes (route-specific: Counter only)TOTAL referenced by /heavy/low: 14664 bytesSame visible UI, same three static widgets — the route-specific chunk is 31x larger (8,967 vs. 287 bytes) purely because "use client" sat on the layout instead of the leaf.
Context providers cross the boundary too
Section titled “Context providers cross the boundary too”React Context is not supported in Server Components — createContext and a <Context.Provider> both need to run somewhere that has React’s client runtime. The fix is not to avoid Context; it’s to wrap only the provider in a small Client Component and render that from a Server Component, same as any other Client Component:
'use client'import { createContext, useState } from 'react'
export const ThemeContext = createContext<{ theme: string; toggle: () => void }>({ theme: 'dark', toggle: () => {},})
export function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setTheme] = useState('dark') const toggle = () => setTheme((t) => (t === 'dark' ? 'light' : 'dark')) return <ThemeContext.Provider value={{ theme, toggle }}>{children}</ThemeContext.Provider>}'use client'import { useContext } from 'react'import { ThemeContext } from './theme-provider'
export function ThemeToggle() { const { theme, toggle } = useContext(ThemeContext) return <button onClick={toggle}>Theme: {theme}</button>}import { ThemeProvider } from './theme-provider'import { ThemeToggle } from './theme-toggle'
// A Server Component renders the provider directly; every Client Component// below it in the tree can read the context with useContext.export default function ThemePage() { return ( <ThemeProvider> <ThemeToggle /> </ThemeProvider> )}Page is a Server Component and never touches useState or createContext itself — it only renders ThemeProvider, which is a Client Component boundary. Render providers as deep in the tree as you can (wrapping only children, not the whole <html> document) so the rest of the static shell stays outside the client bundle.
flowchart LR Root[Server Component root] --> Client["use client boundary"] Root --> Server[More Server Components] Client --> Bundle[Imports join client bundle] Root -. passes children .-> Client Client -. renders server child .-> Server
Mental Model
Section titled “Mental Model”A Server Component is code the browser never sees; a Client Component is code the browser runs. "use client" does not mark a component — it marks the entry point to a whole subtree of the module graph, and everything reachable by import from there ships to the browser. Two escape hatches let content stay server-side even inside a client subtree: children/props carrying already-rendered JSX (the doughnut pattern), and use() unwrapping a Promise that a Server Component started. Props are the only channel across the boundary, and props must be data React can serialize — a function reference and a class instance both fail that test, just in different, real, provable ways.
Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| Server Components by default | Zero client JS for anything that doesn’t need interactivity | Cannot use hooks, state, or browser APIs |
"use client" low in the tree | Smallest possible client bundle | Requires deliberately identifying the interactive leaf, not the whole feature |
use() + <Suspense> for streamed data | Static shell ships immediately, slow data arrives later | Needs a Promise created in a Server Component and a Suspense boundary around the consumer |
server-only / client-only | Wrong-side imports fail the build instead of leaking secrets at runtime | One more (optional) dependency to add |
| Context Provider wrapper | Keeps a shared-state pattern working across the boundary | The wrapper itself, and everything under it, is client JavaScript |
Common Misconceptions
Section titled “Common Misconceptions”- “
'use client'only affects the component it’s on.” It affects every module that component imports and renders directly — the whole subtree joins the client bundle, which is exactly why placement matters (see the 8,967 vs. 287 byte example above). - “Passing a function prop from Server to Client just doesn’t work as expected.” It does not compile silently wrong — it throws a specific, real prerender error:
Event handlers cannot be passed to Client Component props.The one exception is a'use server'Server Function, which crosses as a reference. - “
server-onlyis required for secrets to stay safe.” It’s optional — Next.js already strips non-NEXT_PUBLIC_env vars from the client bundle.server-onlyconverts an accidental wrong-side import into a loud build error, which is a correctness/DX win, not the only safety net. - “React Context doesn’t work with the App Router.” It works fine — it just needs to live inside a Client Component (the provider), which a Server Component can still render directly.
Reading AI-generated code
Section titled “Reading AI-generated code”When a generated snippet crosses the Server/Client boundary, check for these before trusting it:
- Does
"use client"sit on a whole layout or page when only one small piece inside it is actually interactive? That drags every sibling into the client bundle. - Is a function (not a
'use server'Server Function) passed as a prop from a Server Component to a Client Component? That compiles, but throwsEvent handlers cannot be passed to Client Component props.at build/render time. - Is a class instance — an ORM row, a custom error class, anything from
new— passed as a prop across the boundary? Only plain objects and a short list of built-ins survive; classes do not. - Does a file that reads secrets or touches a database have no
server-onlyguard? Nothing stops it from being imported into a Client Component by mistake. - Is
createContext/<Context.Provider>used directly inside a Server Component instead of inside a small'use client'wrapper? - If a Promise is handed to a Client Component for
use(), is there a<Suspense>boundary around the consumer? Without one, the whole tree blocks instead of streaming.
🐛 Spot the bug
'use client'
import { useState } from 'react'import { Sidebar, Footer } from '@/lib/widgets'
export default function Nav({ children }: { children: React.ReactNode }) {const [open, setOpen] = useState(false)return ( <div> <button onClick={() => setOpen((o) => !o)}>Menu</button> {open && <Sidebar />} {children} <Footer /> </div>)}Show the answer
Only the open toggle needs the client — but "use client" sits on the whole Nav component, so Sidebar and Footer (plain presentational components with no hooks of their own) are dragged into the client bundle along with it, even though they never change. Proven in the probe: moving the "use client" boundary down to a small MenuButton leaf and rendering Sidebar/Footer from a Server Component parent shrank the route-specific client chunk from 8,967 bytes to 287 bytes for an equivalent layout — same visible output, 31x less JavaScript. The fix is to isolate the stateful toggle in its own small Client Component and keep Nav, Sidebar, and Footer as Server Components that render it.