Client Components and the 'use client' Directive
In Next.js App Router, 'use client' is perhaps the most consistently misunderstood concept. Many developers read it as "this component only runs in the browser." That interpretation is wrong, and it leads to a cascade of bad architectural decisions. In this chapter we examine the directive from the perspective of the module graph — which is the only way to understand precisely what it does and why.
What 'use client' Actually Means: A Boundary Declaration
'use client' does not mean "this file only executes in the browser." Its precise semantic is: this is the boundary between the server component tree and the client module graph.
React and Next.js maintain two distinct module graphs during the build process:
- Server module graph: All files without
'use client', analyzed at build time as server-only code - Client module graph: All files marked with
'use client', plus every file they import, recursively
When the bundler encounters 'use client', it does the following: stops including that file and its dependencies in the server-side rendering logic, packages them into the client JavaScript bundle, and leaves a reference to the module on the server side.
This is a module graph boundary — a cut point in the dependency tree — not a runtime switch controlling where JavaScript executes.
Client Components Still Server-Render on First Load
This is the second critical insight: components marked with 'use client' still execute on the server during the user's first page visit.
User requests /dashboard for the first time
│
▼
Next.js Server
├── Executes RSC (Server Components)
├── Encounters Client Component → also SSR'd on the server (generates HTML)
└── Returns: full HTML + RSC Payload (includes SSR output of Client Components)
│
▼
Browser receives HTML → immediately renders content (no blank screen)
Browser downloads JS bundle → executes hydration
Hydration complete → Client Components become interactive
The true significance of 'use client' emerges during subsequent client-side navigation: RSC re-executes on the server, while Client Components execute directly in the browser without any server participation in rendering them.
So 'use client' marks where hydration begins, not where rendering begins.
The Module Graph Boundary Propagation Rules
The boundary propagates in one direction:
// lib/database.ts — Server module (no directive)
import { Pool } from 'pg'
export const db = new Pool({ connectionString: process.env.DATABASE_URL })
// components/Button.tsx — Client boundary
'use client'
import { useState } from 'react'
// IMPORTANT: You cannot import server-only modules here.
// If you import db from '@/lib/database', the database driver
// gets bundled into the client JavaScript. Build error or security leak.
export function Button({ label }: { label: string }) {
const [clicked, setClicked] = useState(false)
return (
<button onClick={() => setClicked(true)}>
{clicked ? 'Clicked' : label}
</button>
)
}
// components/Icon.tsx — No 'use client' directive
// But because Button.tsx imports it, Icon.tsx enters the client bundle too.
import { Sparkles } from 'lucide-react'
export function Icon() {
return <Sparkles size={16} />
}
The propagation rule: Every module imported by a 'use client' file — and every module those modules import, recursively — enters the client bundle, even if those modules have no 'use client' directive themselves.
This explains why importing a database driver from a client component is dangerous: the driver and its dependencies get bundled into client JavaScript, potentially exposing connection strings or causing the build to fail because certain Node.js APIs don't exist in browser environments.
Next.js provides the server-only package as a safeguard:
// lib/db.ts
import 'server-only' // Throws a build error if imported from a client module
import { Pool } from 'pg'
export const db = new Pool({ connectionString: process.env.DATABASE_URL })
Passing Server Component Output as children/props
This is the most important pattern in RSC and Client Component collaboration — and the most frequently overlooked.
Wrong approach: importing a Server Component inside a Client Component
// ❌ Wrong: Client Component imports and renders a Server Component
'use client'
import { DataTable } from './DataTable' // Server Component — pulling it into client bundle!
export function Layout() {
return (
<div>
<DataTable /> {/* Cannot run as RSC in the client environment */}
</div>
)
}
When you import a file from a client component, that file gets treated as a client module too — regardless of whether it has 'use client'. The Server Component's async data fetching capabilities are lost.
Correct approach: thread Server Component output through the children prop
// components/Sidebar.tsx — Client Component (needs collapse/expand interaction)
'use client'
import { useState } from 'react'
interface SidebarProps {
children: React.ReactNode // Receives Server Component output
}
export function Sidebar({ children }: SidebarProps) {
const [collapsed, setCollapsed] = useState(false)
return (
<aside className={collapsed ? 'w-12' : 'w-64'}>
<button onClick={() => setCollapsed(c => !c)}>
{collapsed ? 'Expand' : 'Collapse'}
</button>
{/* children is server-rendered content, unaffected by the 'use client' boundary */}
{!collapsed && children}
</aside>
)
}
// app/dashboard/layout.tsx — Server Component
import { Sidebar } from '@/components/Sidebar'
import { NavMenu } from '@/components/NavMenu' // Server Component
import { db } from '@/lib/db'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
// Server Component can query the database
const menuItems = await db.menuItem.findMany({ where: { active: true } })
return (
<div className="flex">
{/* Sidebar is a Client Component, but its children are server-rendered */}
<Sidebar>
<NavMenu items={menuItems} />
</Sidebar>
<main>{children}</main>
</div>
)
}
Why does this work? Because children is computed in the rendering context of DashboardLayout — a Server Component. Its value (a reference to a node in the RSC tree) is passed into Sidebar as a prop. Sidebar simply renders that opaque slot into its JSX without needing to know or control what the slot contains. The server-component nature of the content is preserved.
Common Mistake Patterns
Mistake 1: Wrapping everything in 'use client'
// ❌ Declaring the entire page as a Client Component
'use client'
import { db } from '@/lib/db' // ⚠️ Gets bundled into client JavaScript
export default function Page() {
const [products, setProducts] = useState([])
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts)
}, [])
// The data fetching moved from server to client — worse performance,
// more network requests, larger JS bundle
}
The correct approach keeps the page as a Server Component and marks only the smallest necessary unit as a Client Component:
// ✅ Correct: 'use client' only where needed
// app/products/page.tsx — Server Component
import { db } from '@/lib/db'
import { AddToCartButton } from './AddToCartButton' // This one is a Client Component
export default async function Page() {
const products = await db.product.findMany()
return (
<ul>
{products.map(p => (
<li key={p.id}>
{p.name}
{/* Only the interactive button is a Client Component */}
<AddToCartButton productId={p.id} />
</li>
))}
</ul>
)
}
Mistake 2: Using Context in Server Components
// ❌ Server Components cannot use Context
import { useTheme } from '@/contexts/ThemeContext'
export default function ServerWidget() {
const theme = useTheme() // Runtime error: hooks unavailable in RSC
return <div className={theme}>...</div>
}
Context is a client runtime capability — it relies on React's reconciler, which only runs on the client. Server Components need data to be passed as props explicitly, or read via cookies/headers on the server side.
The Correct Context Provider Architecture
Context cannot be consumed in Server Components, but Context Providers can be structured correctly so that the server component subtree beneath them continues to work:
// providers/ThemeProvider.tsx
'use client'
import { createContext, useContext, useState, useEffect } from 'react'
type Theme = 'light' | 'dark' | 'system'
interface ThemeContextValue {
theme: Theme
setTheme: (theme: Theme) => void
}
const ThemeContext = createContext<ThemeContextValue | null>(null)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('system')
useEffect(() => {
const stored = localStorage.getItem('theme') as Theme | null
if (stored) setTheme(stored)
}, [])
const handleSetTheme = (newTheme: Theme) => {
setTheme(newTheme)
localStorage.setItem('theme', newTheme)
document.documentElement.classList.toggle('dark', newTheme === 'dark')
}
return (
<ThemeContext.Provider value={{ theme, setTheme: handleSetTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
return ctx
}
// app/layout.tsx — Server Component
import { ThemeProvider } from '@/providers/ThemeProvider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{/* ThemeProvider is a client component,
but children (the server component tree) passes through it */}
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
)
}
// components/ThemeToggle.tsx — Client Component, can use useTheme
'use client'
import { useTheme } from '@/providers/ThemeProvider'
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? 'Light mode' : 'Dark mode'}
</button>
)
}
The elegance of this architecture: ThemeProvider is a Client Component. Its children is the server-rendered page content. ThemeToggle is also a Client Component that reads and writes Context. The page content (Server Components) doesn't need to read the Context — it responds to theme changes through CSS variables or classes that ThemeProvider dynamically applies to document.documentElement on the client.
Server Components remain uncontaminated by client concerns. Client Components have the interactivity they need. The two worlds are connected through the children slot.
Interactive Island Architecture
Synthesizing the principles above, the best practice for App Router is the Interactive Island Architecture:
- The page's structural skeleton (layout, data fetching) is built from Server Components, forming a static scaffolding
- Interactive elements (buttons, forms, dropdowns, modals) are Client Components, embedded like islands within the server scaffolding
- Each island is as small as possible, containing only the necessary client-side logic
- Data flows from Server Components into islands via props, or from islands back to the server via Server Actions
Server Skeleton (RSC)
├── Header (Server Component: reads session cookie)
│ └── [SearchBar island: Client, needs real-time search]
├── ProductGrid (Server Component: database query)
│ └── ProductCard (Server Component: static display)
│ └── [AddToCart island: Client, needs click interaction]
└── Footer (Server Component: purely static)
The practical consequence: only the code for SearchBar and AddToCart appears in the client JavaScript bundle. Everything else — the data fetching logic, the database queries, the rendering of static content — stays on the server and never reaches the browser.
This architecture minimizes client JavaScript to the theoretical minimum while maintaining full interactive capability. It is the polar opposite of the "everything is a Client Component" antipattern that developers fall into when first migrating from Pages Router.
When to Use 'use client': A Decision Checklist
Ask these questions about a component:
- Does it use
useState,useReducer, or any other stateful Hook? →'use client' - Does it use
useEffect,useLayoutEffect, or any lifecycle Hook? →'use client' - Does it attach event handlers (
onClick,onChange, etc.)? →'use client' - Does it access browser APIs (
window,document,localStorage)? →'use client' - Does it use a library that requires browser APIs internally? →
'use client' - Does it read from React Context with
useContext? →'use client'
If none of these apply, keep the component as a Server Component. The default should always be: Server Component unless proven otherwise.
Summary
'use client' is a module graph boundary declaration, not a "runs only in the browser" toggle. Client Components still server-render on first page load — the directive marks where hydration begins. The correct composition pattern is threading content through children and props, not importing Server Components from within Client Components. Context must be encapsulated in a Client Provider component, with the server component subtree passed through as children. Understanding how the boundary propagates through the module graph, and how to compose across it using slots, is the core skill for building performant App Router applications.