Server vs Client Components
Understanding when to use Server Components vs Client Components is the most important mental model in modern Next.js.
The Default: Server Components
In the App Router, all components are Server Components by default. They:
- Run only on the server
- Can directly access databases, file systems, and secrets
- Don't add to the JavaScript bundle
- Can be async functions
// This is a Server Component (default)
async function ProductList() {
// Direct database access - no API needed!
const products = await db.query('SELECT * FROM products')
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}
Client Components: When You Need Interactivity
Add 'use client' at the top when you need:
- Event handlers (onClick, onChange, etc.)
- useState, useEffect, or other hooks
- Browser-only APIs (localStorage, window, etc.)
- Third-party libraries that use these
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
)
}
The Mental Model: Think in Boundaries
'use client' creates a boundary. Everything imported into a Client Component becomes part of the client bundle.
// ❌ Bad: Makes the whole page a Client Component
'use client'
import { HeavyChart } from './HeavyChart' // Now in client bundle!
export default function Dashboard() {
const [filter, setFilter] = useState('all')
return <HeavyChart filter={filter} />
}
// ✅ Good: Only the interactive part is a Client Component
// Dashboard is still a Server Component
import { FilteredChart } from './FilteredChart'
export default async function Dashboard() {
const data = await fetchData() // Server-side!
return <FilteredChart data={data} />
}
// FilteredChart.tsx
'use client'
export function FilteredChart({ data }) {
const [filter, setFilter] = useState('all')
// Interactive logic here
}
Composition Pattern: Server in Client
You can pass Server Components as children to Client Components:
// page.tsx (Server Component)
import { ClientWrapper } from './ClientWrapper'
import { ServerContent } from './ServerContent'
export default function Page() {
return (
<ClientWrapper>
<ServerContent /> {/* Still renders on server! */}
</ClientWrapper>
)
}
// ClientWrapper.tsx
'use client'
export function ClientWrapper({ children }) {
const [isOpen, setIsOpen] = useState(true)
return isOpen ? <div>{children}</div> : null
}
When to Use Each
| Server Components | Client Components |
|---|---|
| Fetching data | Event handlers (onClick, etc.) |
| Accessing backend resources | useState, useEffect |
| Keeping secrets on server | Browser APIs (localStorage) |
| Large dependencies | Interactive UI (forms, modals) |
| Static content | Real-time updates |
Common Mistakes
Mistake 1: Making everything a Client Component
// ❌ Don't do this just because you have one interactive part
'use client'
export default async function ProductPage() {
const product = await getProduct() // Can't use async!
// ...
}
Mistake 2: Importing Server-only code in Client Components
'use client'
import { db } from '@/lib/db' // ❌ This will error or expose secrets!
Mistake 3: Not pushing state down
// ❌ Entire page becomes Client Component
'use client'
export default function Page() {
const [search, setSearch] = useState('')
return (
<div>
<input onChange={e => setSearch(e.target.value)} />
<ExpensiveList /> {/* This didn't need to be client! */}
</div>
)
}
// ✅ Only SearchInput is a Client Component
export default function Page() {
return (
<div>
<SearchInput />
<ExpensiveList /> {/* Stays on server */}
</div>
)
}
Summary
- Server Components are the default - use them for data fetching and static content
- Add
'use client'only when you need interactivity or browser APIs 'use client'creates a boundary - everything imported becomes client code- Push client boundaries as far down the tree as possible
- Pass Server Components as children to keep them server-rendered

