Handbooks / Next.js / Chapter 2
Server and Client Components
7 pages · ~5 min✓ Reviewed
Builds on Project Setup and TypeScript. Next up: Data Fetching and Caching.
Server vs Client Components: Mental Model
1. Two Kinds of Component
- Server Component = default in the App Router
- Renders only on the server; ships no component JS
- Can be
asyncandawaitdata directly - Client Component opts in with
'use client' - Pre-rendered to HTML on the server, then hydrates in the browser
- Server renderServer + Client
- HTML sentfirst paint
- HydrateClient only
2. What Each Can Do
| Server | Client | |
|---|---|---|
| Data | async/await, DB, secrets, filesystem | fetch from the browser |
| State | none | useState, useEffect, context |
| Events | none | onClick and other handlers |
| Browser APIs | none | window, localStorage |
| JS shipped | zero | in the bundle |
// Server Component: no directive, async export default async function Page() { const posts = await db.post.findMany() return <PostList posts={posts} /> }
awaits data directly, no useEffect
3. Which One Do I Need?
| Needs | Use |
|---|---|
State, effects, onClick, browser APIs | Client |
| Just fetches and displays data | Server |
Client Components are still pre-rendered on the server. Only Server Components never hydrate.
The 'use client' Boundary
1. The Directive
'use client'must be the first line of the file, above imports- It marks a boundary entry point, not a single component
- Everything the file imports joins the client bundle
- So no directive on each child
'use client' import { useState } from 'react' import Chart from './Chart' // client too export default function Panel() { const [n, setN] = useState(0) }
Chart needs no directive of its own
2. Import Direction
| Importer | Imports | Result |
|---|---|---|
| Server | Client | OK |
| Client | Client | OK |
| Client | Server | NO: it becomes client code |
3. Mistakes
Passing Props Across the Boundary
1. What Can Cross
- Server to Client props must be serializable
- OK: strings, numbers, booleans, plain objects/arrays
- OK:
Date,Map/Set, Promises, JSX elements - No: functions, class instances, event handlers
- Exception: Server Actions marked
'use server'
2. Example
// in a Server Component <Counter initial={await getCount()} /> // works: a number <Button onClick={() => save()} /> // error: a function
data crosses, behaviour does not
3. Props Are Public
- Serverrenders tree
- RSC payloadincludes props
- Browservisible in devtools
- Never pass secrets or whole DB rows
- Pass only the fields the component needs
4. Common Mistake
Composition Patterns
1. Children and Slots
- Children pattern: a Client Component takes
children, a Server Component fills it - The Server child stays on the server
- Slots: any JSX prop works like children, e.g.
sidebar={<ServerSidebar />}
<ClientShell sidebar={<ServerSidebar />}>
<ServerList />
</ClientShell>both Server components, rendered inside a Client shell
2. Providers and Leaves
- Context providers must be Client Components
- Wrap them in
providers.tsxwith'use client', render{children} - The layout stays a Server Component
'use client' export function Providers({ children }) { return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider> }
providers.tsx
- 1CardServer
- 2LikeButtonClient leaf
3. Mistake
What Runs Where and Bundle Impact
1. Request Flow
- Server rendersServer Component tree
- RSC payloadstreamed, client refs
- Browser mergespayload into DOM
- Hydrateclient parts only
- Client Components travel as references, not inline code
- Server Component code and deps (markdown, date libs) cost zero client JS
2. Bundle Impact
| Server Component | Client Component | |
|---|---|---|
| Imports reach browser? | No | Yes |
| Client JS cost | Zero | Itself + everything it imports |
- A heavy library imported in a Client Component inflates the bundle
- Keep heavy libs in Server Components
RSC vs Client Cheat Sheet
1. Default: Server
- Add
'use client'only for state, effects, event handlers, browser APIs, hook libraries
2. The Boundary
'use client'goes at the top of the file, above imports- Marks a boundary; push it down to leaves
| Import | Result |
|---|---|
| Server imports Client | OK |
| Client imports Server | NO: pass as children or props |
3. Props and Providers
- Props must be serializable
- No functions, except Server Actions
- Never pass secrets or whole DB rows
Check yourself
A 'use client' file Shell.tsx does import ServerList from './ServerList' and renders it. What goes wrong, and what is the fix?
- The import turns
ServerListinto client code, so its server-only work breaks or ships to the browser. - Fix: render
<Shell><ServerList /></Shell>from a Server Component and usechildren.
A Server Component renders <Button onClick={() => save()} />, where Button is a Client Component. Result?
- Error: functions are not serializable props.
- Fix: move the handler into
Button, or pass a Server Action marked'use server'.
app/layout.tsx starts with 'use client' and imports Header and Footer (no directive). Where do they run?
- Both join the client bundle, since everything the boundary file imports is client code.
- They are still pre-rendered to HTML on the server. Push
'use client'down to the small interactive leaves.