The App Router's Server Component default confuses a lot of developers coming from Pages Router. Everything is a Server Component unless you opt out. Interactivity requires Client Components. Knowing when to use which — and where to draw the boundary — is the most important architectural decision in an App Router application.
Here's a practical framework for making that decision, including the patterns I've found most useful building generation interfaces at pixova.io.
The Core Distinction
Server Components run on the server, have access to server-side resources (databases, file system, environment variables), and never ship JavaScript to the browser. They render to HTML.
Client Components run in the browser. They can use React hooks (useState, useEffect), handle DOM events, access browser APIs, and create interactive UI.
The practical question isn't "do I want this to be interactive?" It's "what is the minimum client-side JavaScript this component actually needs?"
When to Use Server Components (Default)
Use Server Components for anything that:
Fetches data. Data fetching on the server avoids client-side loading states and keeps data fetching logic close to where data is used.
// Server Component — no 'use client' needed
async function ProductList() {
const products = await db.products.findMany({ where: { active: true } });
return (
<ul>
{products.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
Renders conditionally based on server state. Authorization checks, feature flags, A/B testing logic.
async function AdminSection() {
const user = await getCurrentUser();
if (!user?.isAdmin) return null;
return <AdminDashboard />;
}
Is primarily static HTML. Navigation, headers, footers, content sections, layouts — anything that doesn't need to update or respond to user input.
Needs server-only data. API keys, database connections, server-only environment variables — these should never reach the client.
When to Use Client Components
Opt into Client Components ('use client') for components that need:
React hooks for local state:
'use client';
import { useState } from 'react';
function Accordion({ items }) {
const [openIndex, setOpenIndex] = useState(null);
return (
<div>
{items.map((item, i) => (
<div key={i}>
<button onClick={() => setOpenIndex(i === openIndex ? null : i)}>
{item.title}
</button>
{openIndex === i && <p>{item.content}</p>}
</div>
))}
</div>
);
}
Browser event handlers. Any onClick, onChange, onSubmit, onFocus — these can only run in the browser.
Browser APIs. window, document, localStorage, navigator, geolocation, clipboard API — only available client-side.
Real-time updates. WebSockets, SSE, polling — requires client-side code.
Third-party client-side libraries. Libraries that use browser APIs internally need to run client-side.
The Boundary Pattern
The most important pattern: push 'use client' as deep into the component tree as possible.
Wrong — unnecessarily large client boundary:
// Entire page becomes a Client Component just because of one button
'use client';
import { useState } from 'react';
import { ProductList } from './ProductList'; // Now this is also client-side
export default function ProductPage({ products }) {
const [filter, setFilter] = useState('all');
return (
<div>
<h1>Products</h1>
<FilterButton filter={filter} onChange={setFilter} /> // Needs client
<ProductList products={products} filter={filter} /> // Doesn't need client
</div>
);
}
Right — minimal client boundary:
// Page remains a Server Component
import { FilterableProductList } from './FilterableProductList';
export default async function ProductPage() {
const products = await db.products.findMany();
return (
<div>
<h1>Products</h1>
<FilterableProductList products={products} />
</div>
);
}
// Only the interactive part is a Client Component
'use client';
import { useState } from 'react';
export function FilterableProductList({ products }) {
const [filter, setFilter] = useState('all');
const filtered = products.filter(p =>
filter === 'all' || p.category === filter
);
return (
<div>
<select value={filter} onChange={e => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="sale">Sale</option>
</select>
<ul>
{filtered.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
</div>
);
}
The second version keeps the data fetching on the server while limiting client JavaScript to the interactive parts.
Passing Server Data to Client Components
Server Components can pass data to Client Components through props. The data must be serializable (JSON-compatible):
// Server Component
import { UserProfile } from './UserProfile';
async function UserPage({ userId }) {
const user = await db.users.findUnique({ where: { id: userId } });
// Pass serializable data only — no functions, no class instances
return (
<UserProfile
id={user.id}
name={user.name}
email={user.email}
// Not: user object with methods, Date objects that need serialization
/>
);
}
// Client Component
'use client';
function UserProfile({ id, name, email }) {
const [isEditing, setIsEditing] = useState(false);
// ...
}
What can be passed: strings, numbers, booleans, plain objects, arrays of these.
What can't be passed: functions, class instances, non-serializable objects, React Server Component output.
The Interleaving Pattern
Server Components can render Client Components. Client Components can't render Server Components directly — but they can render children that happen to be Server Components.
// Server Component
import { Modal } from './Modal'; // Client Component
import { ExpensiveServerData } from './ExpensiveServerData'; // Server Component
async function Page() {
return (
<Modal>
<ExpensiveServerData /> {/* Server Component passed as children */}
</Modal>
);
}
// Client Component — receives children without knowing their type
'use client';
function Modal({ children }) {
const [isOpen, setIsOpen] = useState(true);
if (!isOpen) return null;
return (
<div className="modal">
{children} {/* Could be Server Component output */}
<button onClick={() => setIsOpen(false)}>Close</button>
</div>
);
}
This pattern lets interactive Client Components wrap expensive Server Component data fetching without making the data fetching client-side.
Common Mistakes
Adding 'use client' to leaf components unnecessarily. If a component doesn't need hooks or browser APIs, it should be a Server Component. Many developers add it "just in case" and lose the benefits.
Trying to use server-only APIs in Client Components. headers(), cookies(), direct database calls — these don't work in Client Components. Move them up to a Server Component parent.
Passing non-serializable data as props. Passing a full database row object, a Date object, or a function from a Server Component to a Client Component will fail at runtime or cause serialization issues.
Making layout files Client Components. Layout files should almost always be Server Components — they don't need interactivity, and making them client-side forces all nested components into the client bundle unnecessarily.
A Quick Decision Checklist
Before writing 'use client', ask:
- Does this component use
useState,useReducer, or custom hooks that use them? → Client - Does this component use
useEffectoruseLayoutEffect? → Client - Does this component attach browser event handlers? → Client
- Does this component access
window,document, or browser APIs? → Client - Does this component use a library that requires browser APIs? → Client If none of the above: leave it as a Server Component.
Then ask where the boundary belongs:
- Can I split this component so only the interactive part needs
'use client'? - Can I extract the interactive part into a smaller child component? The goal is the smallest possible client bundle — every byte of JavaScript that doesn't need to be in the browser improves page load performance.
Summary
Server Components are the default in App Router — fast, zero JavaScript cost, direct data access. Use them for everything that doesn't require browser interactivity.
Client Components handle interactivity — hooks, events, browser APIs. Use them for the minimum necessary to make UI interactive.
The key architectural discipline: push 'use client' as deep as possible, keep Server Components doing data work, and pass serializable data down through props.
Top comments (0)