- Published on
Understanding use client & use server in Next.js (2025 Deep Guide)
- Authors
- Name
- Armando C. Martin
use client
& use server
in Next.js (2025 Deep Guide)
Understanding "Adding
use client
does not mean your whole page is now client-rendered." – If you only remember one line, make it that.
The App Router (React Server Components + Server Actions) introduced two key directives you apply as file-level or (for server actions) function-level markers (function-level only inside server files):
'use client'
– when placed as the first statement in a file, marks that module as a Client Component (its code is bundled & later hydrated in the browser)'use server'
– when placed as the first statement in a server file (no'use client'
), the file is treated as server-only and exported functions become eligible server actions (they run on the server when used as a formaction
or invoked from a client component). Placing'use server'
as the first statement inside an exported async function body (still in a server file) marks only that function as a server action. You cannot define a server action inside a file already marked'use client'
.
This article demystifies how they actually work together, what they don’t do, and how dynamic imports (next/dynamic
) with ssr: true/false
fit into the picture.
'use client'
Actually Does
Quick Answer: What If you just want the fast, non‑technical answer:
'use client'
tells Next.js: “This component needs JavaScript in the browser so it can react to user actions (clicks, typing, live updates).” It does not mean “render this later” or “send a blank spot first.”
What really happens when a page including a client component loads:
- The server still renders real HTML for the whole page (including the client component’s initial markup). Users (and search engines) see content immediately.
- In that HTML there are subtle markers around the client component. Think of them like translucent overlays saying: “Make this part interactive soon.”
- The browser downloads the small JS chunk for that component, runs it, and “wakes up” (hydrates) that area—event handlers now work, state can change, animations start.
- Only that interactive island gets hydrated; the rest of the page remains light, static HTML (no wasted JS).
You usually see a “hole” / blank only if you intentionally skip server rendering for that piece (e.g. dynamic(..., { ssr: false })
) or you’re waiting on data you deferred behind a Suspense fallback.
So: 'use client'
= send HTML now, add interactivity after. It’s a surgical opt‑in, not a switch that turns the whole page into client-side rendering.
If you need more depth (Flight payload, hydration phases, server actions), keep reading.
TL;DR (Snapshot Mental Model)
- Everything is a Server Component by default (stays on server, not bundled) unless that file starts with
'use client'
. 'use client'
only affects that file + its import subtree; it does not make the whole page client‑rendered.- Server components can import client components (forming a boundary). Client components cannot import server components (prevents leaking secrets); server parents can still compose server-rendered children through props.
- Server Actions (
'use server'
) are RPC-like functions executed on the server; you usually callrevalidatePath
/revalidateTag
to refresh affected UI (uncached data reads will reflect on next navigation, but immediate in-place UI update typically needs explicit revalidation). dynamic(..., { ssr:false })
skips server HTML (hurts SEO/LCP if used for critical content). Prefer SSR + hydration unless strictly browser-only.- Keep client islands thin—but over-fragmentation (hundreds of tiny islands) adds overhead (metadata, requests, scheduling).
- File-level directives must appear before any imports.
Quick Mental Model
Concept | Where It Runs First | Can Access Browser APIs? | Can Use Server Secrets? | Bundled to Client? | Interactive? |
---|---|---|---|---|---|
Server Component | Server only (Flight serialization) | No | Yes | No | No (static markup only) |
Client Component (use client ) | Referenced in Flight (marker) → executed during SSR HTML (if not ssr:false ) → hydrates in browser | Yes | No (unless passed safely) | Yes | Yes |
Server Action ('use server' fn) | Server when invoked | n/a | Yes | No (opaque ref only) | Triggered from client or form |
Dynamic import (ssr: true ) | Server pre-renders (HTML) + then hydrates (if client component) | Yes (if client cmp) | No | Yes | Yes |
Dynamic import (ssr: false ) | Skips server render; client-only load | Yes | No | Yes | Yes |
Key: A
Client Component
is still initially rendered within a server-rendered tree. Only its interactive parts hydrate later.
Default: Everything Is a Server Component (Until You Opt In)
By default in the App Router, every component without a 'use client'
directive at the top of its own source file is treated as a Server Component. This means:
- It runs only on the server (never shipped as JS by itself)
- It can access server-only resources (DB, filesystem, secrets)
- Its rendered output (the resulting JSX) becomes part of the React Flight (RSC) payload
Only when you explicitly add 'use client'
at the top of a module does that module—and the React components & client-only code it transitively imports—enter the client bundle. You cannot “re‑establish” a server boundary beneath a client module via import; you can only compose server-rendered children by having a server parent render both and pass them through props (see Composition section).
Treat
'use client'
as a precision scalpel, not a blanket switch.
use client
Really Does (and Doesn’t)
What ✅ It DOES:
- Tell the bundler to include that module (and its transitive client-only deps) in a browser bundle.
- Create a boundary: any module a client component imports must itself be client-compatible.
- Enable hooks like
useState
,useEffect
,useRef
, event handlers, browser APIs.
❌ It DOES NOT:
- Force the entire page to run on the client.
- Cancel server streaming of the rest of the tree.
- Automatically hydrate before visible (still subject to React scheduling & priority).
- Give you access to server-only things (filesystem, DB) directly.
Example: Mixed Tree
// app/page.tsx (Server Component by default)
import InteractiveCounter from './components/InteractiveCounter'
import PricingTable from './components/PricingTable'
export default async function Home() {
const pricing = await getPricing() // Runs only on server
return (
<div>
<Hero /> {/* Pure server component */}
<PricingTable data={pricing} /> {/* Server component consumes data */}
<InteractiveCounter /> {/* Client boundary – only this subtree hydrates */}
</div>
)
}
// app/components/InteractiveCounter.tsx
'use client'
import { useState } from 'react'
export default function InteractiveCounter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
}
Only
InteractiveCounter
(and its client dependencies) become part of the client JS bundle;Hero
andPricingTable
stay server-only.
'use client'
Under the Hood: Lifecycle When You Add - Build-Time Classification – Compiler scans for directives.
'use client'
marks file as client-tier; it cannot import server-only components. - Module Graph Split – Graph partitions into server graph (executed only server-side, serialized as Flight) & client graph (bundled JS, lazy-loaded).
- Server Render (RSC / Flight Pass) – Only Server Components execute, producing a lightweight Flight payload (serialized hierarchy + props). Client Components are not executed here; a reference marker (module ID + props snapshot) is emitted.
- HTML Streaming (Shell / Optional SSR Pass) – For the initial request, an SSR HTML pass may run. Client components execute once here (unless
ssr:false
) to emit initial HTML. Withssr:false
, a placeholder is streamed instead. - Flight Payload Transfer + Streaming – HTML + Flight chunks stream; Suspense boundaries fill progressively.
- Client Assembly – Browser applies Flight, requests required client JS chunks.
- Selective Hydration – React hydrates each client island independently, attaching event handlers.
- Interactivity Live – Hooks/effects run post-hydration; server-only parts never hydrate.
Result: Adding 'use client'
increases shipped JS and hydration cost only for that boundary and its client subgraph—not the entire page. Keep boundaries shallow for faster hydration scheduling. Avoid deeply nesting client islands unless interaction branching truly requires it.
Import vs Composition (Key Subtlety)
You cannot import a Server Component inside a Client Component module. However, you can still receive server-rendered children through composition:
// Server parent (server by default)
import ClientShell from './ClientShell'
import ServerDetails from './ServerDetails'
export default async function Parent() {
const details = await fetchDetails()
return (
<ClientShell>
{/* Allowed: server parent renders ServerDetails */}
<ServerDetails data={details} />
</ClientShell>
)
}
// ClientShell.tsx
'use client'
export default function ClientShell({ children }: { children: React.ReactNode }) {
return <section className="client-shell">{children}</section>
}
Rule of thumb: Composition (parent renders both) is allowed; direct import inside a client file is not.
Attempting this (invalid):
// ❌ Inside a 'use client' file
import ServerDetails from './ServerDetails' // Build error
Prop Passing Rules & Serialization
Server → Client boundary props must be serializable (structured clone). You cannot pass:
- Class instances
- Functions (except Server Action references)
- Symbols, DOM nodes
If you need derived logic inside a client component, precompute on server and pass plain data objects.
Additional serialization notes:
- Convert Dates to strings (ISO)—don’t rely on implicit revival; reconstruct with
new Date(value)
client-side. - Convert BigInt to string (or number if safe).
- Large arrays: slice / paginate / map to minimal view model.
- Server Action references are opaque markers; function bodies never ship.
- Error objects lose prototype info; return a plain
{ message, code }
shape if you need to surface errors client-side.
Common Anti-Patterns
Anti-Pattern | Why It's Bad | Fix |
---|---|---|
Putting use client at the top of a huge layout | Forces large subtree into client bundle | Split: isolate only interactive islands |
Passing large arrays of raw DB rows directly | Increases hydration payload | Map to minimal view model first |
Client fetching data already fetched server-side | Duplicate waterfalls | Let server components load, then pass down |
Converting to client just to use useEffect for trivial DOM read | Unnecessary JS cost | Use CSS / server computation |
Marking generic utility modules with 'use client' | Forces all consumers into client bundle | Keep utilities directive-free (dual env) |
Using ssr:false for core/above-the-fold content | Loses HTML, hurts SEO & LCP | Keep SSR; reserve ssr:false for browser-only libs |
use server
(Server Actions)?
What About 'use server'
can be file-level (top of file → all exports become actions) or function-level (string literal as first statement inside an exported async function). Either way, the function executes on the server when invoked from a form or client component.
Characteristics
- Serializable reference (RPC-like ID; body not bundled)
- Can access DB, secrets, filesystem
- Return value must be serializable (prefer plain objects / primitives)
- Usually followed by
revalidatePath
/revalidateTag
for UI freshness - Can be sync or async (most are async for I/O)
- Avoids boilerplate API handlers for common mutations
Basic Example
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createTodo(formData: FormData) {
const title = formData.get('title')?.toString() || ''
await db.todo.create({ data: { title } })
revalidatePath('/todos')
return { ok: true }
}
// app/todos/page.tsx (Server Component)
import { createTodo } from '../actions'
export default function Todos() {
return (
<form action={createTodo} className="space-y-2">
<input name="title" placeholder="New todo" />
<button type="submit">Add</button>
</form>
)
}
Form posts directly to the server action—no client fetch code required.
Using in a Client Component (Optimistic UI)
// app/todos/ClientList.tsx
'use client'
import { useTransition, useState } from 'react'
import { createTodo } from '../actions'
export function ClientList({ initial }: { initial: string[] }) {
const [todos, setTodos] = useState(initial)
const [isPending, start] = useTransition()
async function handleAdd(data: FormData) {
const title = data.get('title')?.toString() || ''
setTodos((t) => [...t, title]) // optimistic
start(async () => {
await createTodo(data)
})
}
return (
<form action={handleAdd}>
<input name="title" />
<button disabled={isPending}>Add {isPending && '…'}</button>
<ul>
{todos.map((t) => (
<li key={t}>{t}</li>
))}
</ul>
</form>
)
}
Limitations / Gotchas
Limitation | Notes | Fix |
---|---|---|
Cannot return non-serializable objects | Structured clone constraints | Return plain objects / primitives |
Not for very long streaming responses | Long-lived/streaming not suited for actions | Use a Route Handler / streaming API route |
Error stack shaping | Raw stack may lose context across boundary | Wrap & return { message, code } shape |
Security boundary | Inputs still untrusted | Validate & sanitize server-side |
Must manually revalidate caches | UI not automatically refreshed | Call revalidatePath / revalidateTag |
Form action not firing | Defined inside a 'use client' file | Move action to server file + 'use server' |
Inline Server Action Example
export default function FormWrapper() {
async function submit(data: FormData) {
'use server'
const title = data.get('title')?.toString() || ''
await db.todo.create({ data: { title } })
}
return (
<form action={submit}>
<input name="title" />
<button>Add</button>
</form>
)
}
next/dynamic
Dynamic Imports with Dynamic import is orthogonal to use client
/ use server
but often combined to manage bundle size or avoid SSR for browser-only features.
ssr: true
)
Default Behavior (import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./Chart')) // SSR + hydrate later
- If
Chart.tsx
has'use client'
, it gets server-rendered HTML first (better FCP) + then hydrates. - Using
dynamic
for pure server components is rarely necessary—Suspense
+ streaming already handles latency.
ssr: false
)
Client-Only Load (const Map = dynamic(() => import('./Map'), { ssr: false })
- Skips server render: sends placeholder → client loads JS → renders.
- Worsens LCP + SEO if used for above-the-fold content.
- Increases risk of layout shift unless you provide a skeleton.
ssr: false
When to Use Scenario | Good? | Why |
---|---|---|
Heavy library relying on window (Leaflet, map libs) | ✅ | Avoids shims / conditional wrappers |
Non-critical below-the-fold widget | ✅ | Defers execution cost |
Core hero content | ❌ | Hurts SEO & perceived perf |
SEO-sensitive pricing/heading | ❌ | No HTML for crawlers |
use client
Combining with // Map.tsx
'use client'
export function Map() {
/* browser APIs */
}
const Map = dynamic(() => import('./Map'), { ssr: false, loading: () => <p>Loading map…</p> })
use client
enables hydration;ssr:false
ensures no server HTML attempt. Provide a skeleton.
ssr: false
= better performance”
Misconception: “Often false. You trade initial HTML + SEO + early paint for reduced server work. Measure first.
Putting It Together: Mixed Example
// app/dashboard/page.tsx
import dynamic from 'next/dynamic'
import { Suspense } from 'react'
import Stats from './Stats' // Server Component
const LiveVisitors = dynamic(() => import('./LiveVisitors'), { ssr: true }) // client + SSR
const GeoMap = dynamic(() => import('./GeoMap'), { ssr: false }) // heavy client-only
export default async function Dashboard() {
const stats = await getStats()
return (
<div>
<Stats data={stats} />
<Suspense fallback={<p>Loading live…</p>}>
<LiveVisitors />
</Suspense>
<GeoMap />
</div>
)
}
// app/dashboard/LiveVisitors.tsx
'use client'
import { useEffect, useState } from 'react'
export default function LiveVisitors() {
const [n, setN] = useState(0)
useEffect(() => {
const id = setInterval(() => setN((x) => x + 1), 1000)
return () => clearInterval(id)
}, [])
return <p>Live visitors: {n}</p>
}
Performance Tips
- Push logic up: derive data server-side; send lean props.
- Thin client boundaries: many small islands > one giant layout with
use client
. - Stream + Suspense: server tree streams while client islands hydrate incrementally.
- Avoid unnecessary
useEffect
: prefer server computation or CSS. - Don’t over-fragment: too many micro islands introduce overhead (chunk refs, scheduling).
- Share pure utilities: directive-free helpers stay dual-runtime.
- Minimize state scope: keep state local; avoid lifting static markup into a large client wrapper.
- Measure: bundle analyzer + React Profiler; optimize from data, not intuition.
- Inspect bundles: enable
@next/bundle-analyzer
to spot unintended client code growth.
FAQ
Does use client
inside a file affect imported siblings? No. Scope is file-level only (and its re-exports).
Can a client component import a server component? No – direction is one-way: server → client. Use composition.
Are Server Actions replacing API routes? Not entirely. Keep API routes for external integrations, streaming responses, webhooks.
Is ssr:false
the same as full CSR? Only for that component subtree; surrounding server segments still SSR.
Where must directives appear? First statement (before imports) for file-level effect; inside a function body (first statement) for function-level server action.
Can utilities be shared by server & client? Yes—pure side‑effect‑free modules compile per environment.
Can I have both 'use client'
and 'use server'
at file top? No—file-level directives are mutually exclusive. Use function-level 'use server'
inside a server file instead.
Do client islands disable static optimization around them? No—static server segments remain cacheable; only dynamic data access marks segments dynamic.