Techify Blog Logo
Published on

Understanding use client & use server in Next.js (2025 Deep Guide)

Authors
  • avatar
    Name
    Armando C. Martin
    Twitter

Understanding use client & use server in Next.js (2025 Deep Guide)

"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 form action 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.


Quick Answer: What 'use client' Actually Does

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:

  1. The server still renders real HTML for the whole page (including the client component’s initial markup). Users (and search engines) see content immediately.
  2. In that HTML there are subtle markers around the client component. Think of them like translucent overlays saying: “Make this part interactive soon.”
  3. 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.
  4. 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)

  1. Everything is a Server Component by default (stays on server, not bundled) unless that file starts with 'use client'.
  2. 'use client' only affects that file + its import subtree; it does not make the whole page client‑rendered.
  3. 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.
  4. Server Actions ('use server') are RPC-like functions executed on the server; you usually call revalidatePath / revalidateTag to refresh affected UI (uncached data reads will reflect on next navigation, but immediate in-place UI update typically needs explicit revalidation).
  5. dynamic(..., { ssr:false }) skips server HTML (hurts SEO/LCP if used for critical content). Prefer SSR + hydration unless strictly browser-only.
  6. Keep client islands thin—but over-fragmentation (hundreds of tiny islands) adds overhead (metadata, requests, scheduling).
  7. File-level directives must appear before any imports.

Quick Mental Model

ConceptWhere It Runs FirstCan Access Browser APIs?Can Use Server Secrets?Bundled to Client?Interactive?
Server ComponentServer only (Flight serialization)NoYesNoNo (static markup only)
Client Component (use client)Referenced in Flight (marker) → executed during SSR HTML (if not ssr:false) → hydrates in browserYesNo (unless passed safely)YesYes
Server Action ('use server' fn)Server when invokedn/aYesNo (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)NoYesYes
Dynamic import (ssr: false)Skips server render; client-only loadYesNoYesYes

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.


What use client Really Does (and Doesn’t)

✅ 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 and PricingTable stay server-only.

Under the Hood: Lifecycle When You Add 'use client'

  1. Build-Time Classification – Compiler scans for directives. 'use client' marks file as client-tier; it cannot import server-only components.
  2. Module Graph Split – Graph partitions into server graph (executed only server-side, serialized as Flight) & client graph (bundled JS, lazy-loaded).
  3. 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.
  4. 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. With ssr:false, a placeholder is streamed instead.
  5. Flight Payload Transfer + Streaming – HTML + Flight chunks stream; Suspense boundaries fill progressively.
  6. Client Assembly – Browser applies Flight, requests required client JS chunks.
  7. Selective Hydration – React hydrates each client island independently, attaching event handlers.
  8. 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-PatternWhy It's BadFix
Putting use client at the top of a huge layoutForces large subtree into client bundleSplit: isolate only interactive islands
Passing large arrays of raw DB rows directlyIncreases hydration payloadMap to minimal view model first
Client fetching data already fetched server-sideDuplicate waterfallsLet server components load, then pass down
Converting to client just to use useEffect for trivial DOM readUnnecessary JS costUse CSS / server computation
Marking generic utility modules with 'use client'Forces all consumers into client bundleKeep utilities directive-free (dual env)
Using ssr:false for core/above-the-fold contentLoses HTML, hurts SEO & LCPKeep SSR; reserve ssr:false for browser-only libs

What About use server (Server Actions)?

'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

LimitationNotesFix
Cannot return non-serializable objectsStructured clone constraintsReturn plain objects / primitives
Not for very long streaming responsesLong-lived/streaming not suited for actionsUse a Route Handler / streaming API route
Error stack shapingRaw stack may lose context across boundaryWrap & return { message, code } shape
Security boundaryInputs still untrustedValidate & sanitize server-side
Must manually revalidate cachesUI not automatically refreshedCall revalidatePath / revalidateTag
Form action not firingDefined inside a 'use client' fileMove 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>
  )
}

Dynamic Imports with next/dynamic

Dynamic import is orthogonal to use client / use server but often combined to manage bundle size or avoid SSR for browser-only features.

Default Behavior (ssr: true)

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.

Client-Only Load (ssr: false)

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.

When to Use ssr: false

ScenarioGood?Why
Heavy library relying on window (Leaflet, map libs)Avoids shims / conditional wrappers
Non-critical below-the-fold widgetDefers execution cost
Core hero contentHurts SEO & perceived perf
SEO-sensitive pricing/headingNo HTML for crawlers

Combining with use client

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

Misconception: “ssr: false = better performance”

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.