Skip to content

Server and Client Components

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.

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.

app/dashboard/page.tsx
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.

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.

app/counter.tsx
'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.

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

app/client-shell.tsx
'use client';
export default function ClientShell({ children }: { children: React.ReactNode }) {
return <div className="shell">{children}</div>;
}
app/server-data.tsx
export default function ServerData() {
return <p>Rendered on the server</p>;
}
app/page.tsx
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.

lib/notifications-data.ts
export async function getNotifications(userId: string) {
await new Promise((r) => setTimeout(r, 20));
return [{ id: '1', text: `Hello ${userId}` }, { id: '2', text: 'Another one' }];
}
app/notifications/notification-list.tsx
'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>
)
}
app/notifications/page.tsx
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:

lib/secret-data.ts
import 'server-only'
export function getSecret() {
return process.env.SECRET_KEY ?? 'unset'
}
app/exp1/client-uses-server-only.tsx
'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 is
only available in Server Components in the App Router, but you are using it
in 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 module
It 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:

app/exp2/client-receiver.tsx
'use client'
export default function ClientReceiver({ onSave }: { onSave: () => void }) {
return <button onClick={onSave}>Save</button>
}
app/exp2/page.tsx
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:

app/exp3/client-receiver-user.tsx
'use client'
class UserModel {
constructor(public name: string) {}
}
export default function ClientReceiver({ user }: { user: UserModel }) {
return <p>{user.name}</p>
}
app/exp3/page.tsx
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 Client
Components from Server Components. Classes or null prototypes are not
supported.
{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.

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.

lib/widgets.tsx
// 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>
}
app/heavy/high/layout.tsx
'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>
)
}
app/heavy/low/layout.tsx
// 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>
)
}
app/heavy/low/counter.tsx
'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 bytes

Same 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.

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:

app/theme/theme-provider.tsx
'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>
}
app/theme/theme-toggle.tsx
'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>
}
app/theme/page.tsx
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
Where the client boundary sits

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.

What you getBenefitCost
Server Components by defaultZero client JS for anything that doesn’t need interactivityCannot use hooks, state, or browser APIs
"use client" low in the treeSmallest possible client bundleRequires deliberately identifying the interactive leaf, not the whole feature
use() + <Suspense> for streamed dataStatic shell ships immediately, slow data arrives laterNeeds a Promise created in a Server Component and a Suspense boundary around the consumer
server-only / client-onlyWrong-side imports fail the build instead of leaking secrets at runtimeOne more (optional) dependency to add
Context Provider wrapperKeeps a shared-state pattern working across the boundaryThe wrapper itself, and everything under it, is client JavaScript
  • “'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-only is required for secrets to stay safe.” It’s optional — Next.js already strips non-NEXT_PUBLIC_ env vars from the client bundle. server-only converts 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.

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 throws Event 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-only guard? 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

app/nav.tsx
'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.

In the App Router, what is a component before any directive is added?
What does "use client" do to the modules a file imports?
How can a Client Component include Server-Component content?
What happens when this builds?
// app/exp2/page.tsx
function saveOnServer() {
  console.log('saved')
}

export default function Page() {
  return <ClientReceiver onSave={saveOnServer} />
}
Why does importing a server-only-guarded module from a Client Component fail the build?
Why is getNotifications called without await here?
// lib/notifications-data.ts
export async function getNotifications(userId: string) {
  await new Promise((r) => setTimeout(r, 20))
  return [{ id: '1', text: `Hello ${userId}` }]
}
// app/notifications/page.tsx
export default function NotificationsPage() {
  const notificationsPromise = getNotifications('u_1')
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <NotificationList notificationsPromise={notificationsPromise} />
    </Suspense>
  )
}
Two equivalent layouts render the same Nav/Sidebar/Footer and one interactive button. One puts "use client" on the whole layout; the other keeps the layout on the server and marks only the button's wrapper as a Client Component. What does the probe measurement show?