Server vs Client Components
The App Router changed everything. Here's what actually happens and how to avoid common mistakes.
Mental Model
Server Components are the default. They run only on the server. Client Components ('use client') still SSR, but they also hydrate and can use hooks and events.
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.
Quick Checklist
Audit 'use client' usage
Only components with interactivity (hooks, events) should have this directive.
Extract interactive parts Keep client components small. Move static content to server components.
Protect browser APIs
Wrap window/document access in typeof window !== 'undefined' checks or useEffect.
Handle browser-only libraries
Use dynamic import with ssr: false for libraries that access browser APIs on import.
Check for hydration errors Open the console and fix any hydration mismatch warnings.
Minimize client bundle Keep large static content in Server components. Lazy load heavy libraries.
With fundamentals covered, see Caching Strategies for Vercel-specific optimizations.