React Server vs Client Components: When to Use What
TL;DR
- Components are Server Components by default in Next.js App Router. Add
'use client'only when you need browser APIs, hooks, or event handlers- Server Components add zero bytes to the JavaScript bundle, reducing page weight and improving LCP
- The composition pattern (server component wrapping client component, passing data as props) is the key architecture skill
- Only data that is serialisable (JSON-compatible) can cross the server-to-client boundary
Table of Contents
- The Mental Model Shift
- What Server Components Can and Cannot Do
- What Client Components Can and Cannot Do
- The Composition Pattern
- Performance Implications
- Common Mistakes
- The Serialisation Boundary
- Architecture Example
- Server Actions vs Server Components
- Frequently Asked Questions
The Mental Model Shift
Before React Server Components, every React component was a client component. Your entire component tree shipped as JavaScript to the browser, hydrated, and became interactive. This was simple to understand but expensive in bundle size.
With Server Components in Next.js App Router, the default flipped. Every component is a Server Component unless you explicitly opt out with 'use client'. Server Components run on the server during rendering, produce HTML, and send zero JavaScript to the browser.
Think of it as two zones:
Server zone: Components that fetch data, access databases, read files, and produce static HTML. They never re-render on the client. They cannot use useState, useEffect, or any browser API.
Client zone: Components that handle user interaction, manage local state, use browser APIs, and re-render in the browser. They ship JavaScript and hydrate like traditional React components.
The 'use client' directive is the boundary between these zones. Everything above it (in the import tree) stays on the server. Everything below it (including its children) runs on the client.
What Server Components Can and Cannot Do
Can do:
async/awaitdirectly in the component body (fetch data inline)- Access databases, file systems, and server-only APIs
- Use server-only packages (Node.js built-ins, database drivers)
- Render HTML without adding to the client JavaScript bundle
- Pass React elements (JSX) as props to client components
Cannot do:
- Use React hooks (
useState,useEffect,useRef,useContext) - Attach event handlers (
onClick,onChange,onSubmit) - Use browser APIs (
window,document,localStorage) - Use third-party libraries that depend on browser APIs
- Re-render in response to user interaction
// This is a Server Component (default in App Router)
import { db } from '@/lib/database';
export default async function UserList() {
const users = await db.user.findMany(); // Direct database access
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
);
}
What Client Components Can and Cannot Do
Can do:
- Use all React hooks
- Handle user events (clicks, inputs, gestures)
- Access browser APIs
- Manage local UI state
- Use third-party UI libraries (date pickers, rich text editors, charts)
Cannot do:
- Access the file system or databases directly (security risk)
- Use server-only packages
- Be async (no
async function Component()) - Import Server Components (but can receive them as
childrenprops)
'use client';
import { useState } from 'react';
export function SearchFilter({ onSearch }: { onSearch: (query: string) => void }) {
const [query, setQuery] = useState('');
return (
<input
value={query}
onChange={(e) => {
setQuery(e.target.value);
onSearch(e.target.value);
}}
placeholder="Search users..."
/>
);
}
The Composition Pattern
The key skill with Server Components is the composition pattern: a Server Component that wraps Client Components and passes server-fetched data as props.
// page.tsx (Server Component)
import { db } from '@/lib/database';
import { UserTable } from './user-table'; // Client Component
export default async function UsersPage() {
const users = await db.user.findMany({
select: { id: true, name: true, email: true, role: true }
});
// Server fetches data, Client renders interactive table
return <UserTable initialUsers={users} />;
}
// user-table.tsx (Client Component)
'use client';
import { useState } from 'react';
export function UserTable({ initialUsers }: { initialUsers: User[] }) {
const [sortField, setSortField] = useState<keyof User>('name');
const [filterRole, setFilterRole] = useState<string>('all');
const filtered = initialUsers
.filter(u => filterRole === 'all' || u.role === filterRole)
.sort((a, b) => String(a[sortField]).localeCompare(String(b[sortField])));
return (
<div>
<select value={filterRole} onChange={e => setFilterRole(e.target.value)}>
<option value="all">All Roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
<table>
<thead>
<tr>
<th onClick={() => setSortField('name')}>Name</th>
<th onClick={() => setSortField('email')}>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
{filtered.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.role}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
The database query runs on the server, adds zero JavaScript to the bundle, and exposes no database credentials to the client. The interactive sorting and filtering run on the client with the pre-fetched data.
Performance Implications
Bundle size reduction: A Server Component with 500 lines of code and 10 imported utility functions adds 0 bytes to the client JavaScript bundle. The same component as a client component might add 15-50KB depending on its dependencies.
LCP improvement: Server Components produce HTML immediately, which the browser can paint without waiting for JavaScript to download, parse, and execute. This directly improves Largest Contentful Paint.
TTFB consideration: Server Components shift computation to the server, which can increase Time to First Byte if your server is slow or far from the user. Use streaming (with Suspense) to start sending HTML before all data is ready.
Hydration cost: Client Components must hydrate (attach event listeners and reconcile the DOM). More client components means more hydration work, which can block interactivity (measured by INP). Fewer client components means faster interactivity.
Common Mistakes
Marking too many components as client
// BAD: The entire page is a client component because of one useState
'use client';
export default function ProductPage({ params }) {
const [quantity, setQuantity] = useState(1);
// ...200 lines of product display that doesn't need client JS
}
// GOOD: Only the interactive part is a client component
export default async function ProductPage({ params }) {
const product = await fetchProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<QuantitySelector /> {/* Only this needs 'use client' */}
</div>
);
}
Importing server-only code in client components
Client components cannot import modules that use Node.js APIs. The server-only package helps enforce this:
// lib/database.ts
import 'server-only'; // Throws build error if imported from client component
import { PrismaClient } from '@prisma/client';
export const db = new PrismaClient();
Using context incorrectly
useContext only works in client components. If you need to share data across server components, pass it as props or use the cookies() / headers() functions.
The Serialisation Boundary
Data passed from Server Components to Client Components must be serialisable - it passes through the React Server Components wire format, which supports JSON-compatible types.
Serialisable: strings, numbers, booleans, null, arrays, plain objects, Date, Map, Set, TypedArrays, ReadableStream, Promise.
Not serialisable: functions, classes with methods, React elements from server components (they are already serialised), symbols, Error objects.
// This works
<ClientComponent data={{ name: "test", createdAt: new Date() }} />
// This does NOT work
<ClientComponent
onSubmit={() => console.log('clicked')} // Function cannot cross boundary
dbClient={prisma} // Class with methods cannot cross boundary
/>
Architecture Example
Here is a complete page architecture using both server and client components:
page.tsx (Server) - fetches data, renders layout
Header.tsx (Server) - static navigation, no interactivity
MobileMenu.tsx (Client) - hamburger menu toggle needs useState
ProductGrid.tsx (Server) - maps over products, pure rendering
ProductCard.tsx (Server) - displays product info
AddToCart.tsx (Client) - button with onClick + cart state
Sidebar.tsx (Server) - category list, static
PriceFilter.tsx (Client) - range slider needs useState
Footer.tsx (Server) - static footer content
Five out of eight components are Server Components that add zero JavaScript to the bundle. Only the three interactive components ship client-side code.
When building your Next.js API routes that serve data to these components, test your API endpoints with our free API tester and validate the JSON response structure with our formatter.
For details on upgrading to the latest Next.js version, see our Next.js 15 App Router migration guide.
The Debuggers builds Next.js applications using modern React patterns, including Server Components architecture for optimal performance and SEO.
Server Actions vs Server Components
Server Actions ('use server') and Server Components are different concepts that often cause confusion:
Server Components: Run on the server during rendering. They produce HTML. They are for reading data.
Server Actions: Functions that run on the server in response to user actions (form submissions, button clicks). They are for writing data (mutations).
// Server Action (for mutations)
'use server';
export async function createUser(formData: FormData) {
await db.user.create({ data: { name: formData.get('name') as string } });
revalidatePath('/users');
}
// Server Component (for reads)
export default async function UsersPage() {
const users = await db.user.findMany();
return <UserList users={users} createAction={createUser} />;
}
Frequently Asked Questions
Can a Client Component render a Server Component?
Not directly through import. A Client Component cannot import and render a Server Component because the client bundle does not include server-side code. However, a Client Component can render Server Components that are passed as children or other React element props. This is the "donut pattern" where a client component wraps a hole that server components fill from above.
Do Server Components replace API routes?
For data fetching within your own application, yes. Server Components can query databases directly without needing an API endpoint as an intermediary. API routes are still needed for external consumers (mobile apps, third-party integrations) and for webhooks. The pattern is: Server Components for your own Next.js pages, API routes for external access.
How do Server Components affect SEO?
Server Components improve SEO significantly. They render HTML on the server, which search engines receive immediately without executing JavaScript. This means your content is fully indexable with correct meta tags and heading structure. Client Components that render content dynamically may not be indexed by all search engines, though Google's crawler does execute JavaScript.
Should I convert my entire app to Server Components?
No. Convert components that are data-fetching wrappers, static content displays, and layout shells. Keep components that handle user interaction, manage form state, or use browser APIs as client components. The goal is not zero client components but minimal, well-scoped client components that contain only the interactive logic they need.
Building with React Server Components?
Validate your API response structures with our free JSON Formatter before designing your component data flow. Ensure your serialisable props match the data your server fetches.
Need help architecting a Next.js application? The Debuggers provides React and Next.js consulting for teams adopting modern patterns.
Found this helpful?
Join thousands of developers using our tools to write better code, faster.