Server vs Client Components
The App Router changed everything. Here's what actually happens and how to avoid common mistakes.
TL;DR
Server Components are the default - they run on server only. 'use client' doesn't mean client-only, it means "needs hydration." Keep client boundaries small. Use the children pattern for wrappers.
The Two Types
// No directive = Server Component (default)
export default async function ProductList() {
const products = await db.products.findMany();
return <ul>{products.map(p => <li>{p.name}</li>)}</ul>;
}- Runs on server only
- Can use
async/awaitdirectly - Can access DB, filesystem, secrets
- Cannot use
useState,useEffect,onClick
'use client';
export default function AddToCart({ productId }) {
const [loading, setLoading] = useState(false);
return (
<button onClick={() => setLoading(true)}>
Add to Cart
</button>
);
}- Renders on server first (SSR), hydrates on client
- Can use all React hooks
- Can use browser APIs
- Cannot be
async
'use client' doesn't mean "client-only". It means "this component needs JavaScript to be interactive." It still SSRs.
The Boundary Rule
Client components cannot import Server components:
'use client';
import ServerThing from './ServerThing'; // ❌ BreaksBut Server components can render Client components:
// Server Component
import ClientButton from './ClientButton';
export default async function Page() {
const data = await getData();
return (
<div>
<h1>{data.title}</h1>
<ClientButton /> {/* ✅ Works */}
</div>
);
}The Children Pattern
Need a Client wrapper around Server content? Use children:
'use client';
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}export default function Page() {
return (
<ThemeProvider>
<ServerContent /> {/* ✅ Works via children */}
</ThemeProvider>
);
}Common Mistakes
Mistake 1: Browser APIs Without Checks
'use client';
// ❌ Breaks during SSR
const width = window.innerWidth;
// ✅ Check environment
const width = typeof window !== 'undefined' ? window.innerWidth : 0;
// ✅ Or use useEffect
const [width, setWidth] = useState(0);
useEffect(() => setWidth(window.innerWidth), []);Mistake 2: Browser-Only Libraries
'use client';
// ❌ Library accesses window on import
import Confetti from 'react-confetti';
// ✅ Dynamic import with ssr: false
import dynamic from 'next/dynamic';
const Confetti = dynamic(() => import('react-confetti'), { ssr: false });Mistake 3: Hydration Mismatch
'use client';
// ❌ Server and client render different values
const [time, setTime] = useState(new Date().toISOString());
// ✅ Initialize empty, set on client
const [time, setTime] = useState<string | null>(null);
useEffect(() => setTime(new Date().toISOString()), []);Mistake 4: Unnecessary Client Components
// ❌ No interactivity - doesn't need 'use client'
'use client';
export default function ProductCard({ product }) {
return (
<div>
<h2>{product.name}</h2>
<p>{product.price}</p>
</div>
);
}
// ✅ Remove the directive - let it be a Server Component
export default function ProductCard({ product }) {
return (
<div>
<h2>{product.name}</h2>
<p>{product.price}</p>
</div>
);
}Only add 'use client' when you need hooks or event handlers.
Decision Guide
| Your Component Needs | Use |
|---|---|
| Database/API calls | Server |
useState, useEffect | Client |
onClick, onChange | Client |
| Just displays props | Server |
Uses window/document | Client |
| Reads cookies/headers | Server |
The Composition Pattern
Keep client boundaries small:
// ❌ Entire page is client
'use client';
export default function ProductPage() {
const [qty, setQty] = useState(1);
// ... lots of static content ...
return <div>...</div>;
}
// ✅ Only interactive part is client
// ProductPage.tsx (Server Component)
export default async function ProductPage() {
const product = await getProduct();
return (
<div>
<h1>{product.name}</h1> {/* Server */}
<p>{product.description}</p> {/* Server */}
<QuantityPicker /> {/* Client */}
</div>
);
}
// QuantityPicker.tsx
'use client';
export function QuantityPicker() {
const [qty, setQty] = useState(1);
return <input value={qty} onChange={e => setQty(e.target.value)} />;
}Less client JavaScript = faster page loads.
Key Takeaways
Audit 'use client' usage
Only components with hooks or event handlers need this directive.
Protect browser APIs
Wrap window/document in typeof window !== 'undefined' or useEffect.
Handle browser-only libraries
Use dynamic(() => import(...), { ssr: false }) for libraries that access browser APIs on import.
Fix hydration mismatches
Check console for warnings. Initialize client-dependent state in useEffect.
Keep client boundaries small Extract only the interactive parts into Client components.
With fundamentals covered, see Caching Strategies for Vercel-specific optimizations.