Understanding Server Components vs Client Components
Next.js 13 introduced the React Server Components (RSC) architecture, fundamentally changing how we think about component rendering. This lesson explores the differences between Server Components and Client Components, when to use each, and how they work together.
What Are Server Components?
Server Components are React components that render exclusively on the server. They never send JavaScript to the client, making them incredibly efficient for data fetching and reducing bundle sizes.
Key Concept: By default, all components in the Next.js App Router are Server Components. This is a fundamental shift from the traditional React model where everything runs on the client.
Benefits of Server Components
- Zero JavaScript Bundle: Server Components don't add any JavaScript to the client bundle
- Direct Backend Access: Can directly access databases, file systems, and server-only resources
- Improved Security: Sensitive data and API keys never reach the client
- Better Performance: Data fetching happens on the server, closer to data sources
- Automatic Code Splitting: Only Client Components are split and loaded on demand
Server Component Example
// app/posts/page.tsx (Server Component by default)
import { prisma } from '@/lib/prisma';
export default async function PostsPage() {
// Direct database access - no API route needed
const posts = await prisma.post.findMany({
orderBy: { createdAt: 'desc' },
take: 10
});
return (
<div>
<h1>Recent Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
</div>
);
}
What Are Client Components?
Client Components are traditional React components that render on the client side. They enable interactivity, browser APIs, and React hooks like useState and useEffect.
When to Use Client Components
You need Client Components when your component requires:
- Interactivity: Click handlers, form inputs, event listeners
- State Management: useState, useReducer, useContext
- Effects: useEffect for side effects, subscriptions, timers
- Browser APIs: localStorage, window, navigator, geolocation
- Custom Hooks: Any hooks that depend on client-side features
- Class Components: Lifecycle methods (though rare in modern React)
Best Practice: Use Server Components by default and only opt into Client Components when you need interactivity or browser-specific features. This keeps your bundle size minimal.
The 'use client' Directive
To mark a component as a Client Component, add the 'use client' directive at the top of the file:
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Important: The 'use client' directive only needs to be added to files that use client-side features. Any components imported into a Client Component automatically become Client Components.
Composition Patterns
Understanding how to compose Server and Client Components is crucial for building efficient Next.js applications.
Pattern 1: Server Component with Client Component Children
// app/dashboard/page.tsx (Server Component)
import { getUser } from '@/lib/auth';
import Counter from '@/components/Counter'; // Client Component
export default async function DashboardPage() {
const user = await getUser(); // Server-side data fetching
return (
<div>
<h1>Welcome, {user.name}!</h1>
{/* Server-rendered content */}
<p>Your email: {user.email}</p>
{/* Client Component for interactivity */}
<Counter />
</div>
);
}
Pattern 2: Passing Server Components as Props
You can pass Server Components as children or props to Client Components:
// app/layout.tsx (Server Component)
import Sidebar from '@/components/Sidebar'; // Client Component
import Feed from '@/components/Feed'; // Server Component
export default function Layout({ children }) {
return (
<html>
<body>
{/* Pass Server Component as children */}
<Sidebar>
<Feed /> {/* This stays a Server Component */}
</Sidebar>
{children}
</body>
</html>
);
}
// components/Sidebar.tsx (Client Component)
'use client';
import { useState } from 'react';
export default function Sidebar({ children }) {
const [collapsed, setCollapsed] = useState(false);
return (
<aside className={collapsed ? 'collapsed' : ''}>
<button onClick={() => setCollapsed(!collapsed)}>
Toggle
</button>
{children} {/* Server Component rendered here */}
</aside>
);
}
Why This Works: The Client Component doesn't need to know what 'children' is. It just renders it. The Server Component is serialized and passed as props, maintaining its server-side nature.
Pattern 3: Sharing Data Between Components
// app/products/page.tsx (Server Component)
import { getProducts } from '@/lib/db';
import ProductList from '@/components/ProductList'; // Client Component
export default async function ProductsPage() {
const products = await getProducts();
// Pass server-fetched data as props to Client Component
return <ProductList initialProducts={products} />;
}
// components/ProductList.tsx (Client Component)
'use client';
import { useState } from 'react';
export default function ProductList({ initialProducts }) {
const [products, setProducts] = useState(initialProducts);
const [filter, setFilter] = useState('');
const filteredProducts = products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
return (
<div>
<input
type="text"
placeholder="Filter products..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<ul>
{filteredProducts.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}
Data Flow and Serialization
When passing data from Server Components to Client Components, Next.js serializes the data. This means only JSON-serializable data can be passed:
Cannot Pass:
- Functions
- Dates (will be converted to strings)
- Class instances
- undefined values
- Symbols
// ❌ This will NOT work
export default async function Page() {
const handleClick = () => console.log('clicked');
return <ClientComponent onClick={handleClick} />;
}
// ✅ This works - define the handler in the Client Component
// Server Component
export default async function Page() {
return <ClientComponent />;
}
// Client Component
'use client';
export default function ClientComponent() {
const handleClick = () => console.log('clicked');
return <button onClick={handleClick}>Click</button>;
}
Network Boundary
The 'use client' directive creates a boundary between server and client code. Everything imported into a Client Component becomes part of the client bundle:
// components/DataDisplay.tsx
'use client'; // This marks the boundary
import { format } from 'date-fns'; // Will be included in client bundle
import { useState } from 'react';
export default function DataDisplay({ data }) {
const [expanded, setExpanded] = useState(false);
return (
<div>
<button onClick={() => setExpanded(!expanded)}>
Toggle
</button>
{expanded && (
<p>{format(new Date(data.date), 'PPP')}</p>
)}
</div>
);
}
Optimization Tip: If you only need client-side code in part of your component tree, create separate Client Components for just those parts. This keeps as much code as possible in Server Components.
Common Pitfalls
1. Importing Server-Only Code in Client Components
// ❌ This will cause an error
'use client';
import { prisma } from '@/lib/prisma'; // Server-only library
export default function Component() {
// This will fail - prisma can't run in the browser
const data = await prisma.user.findMany();
return <div>{data}</div>;
}
2. Using 'use client' Unnecessarily
// ❌ Unnecessary - no client features used
'use client';
export default function Header({ title }) {
return <h1>{title}</h1>;
}
// ✅ Better - keep as Server Component
export default function Header({ title }) {
return <h1>{title}</h1>;
}
3. Trying to Import Server Components into Client Components
// ❌ This will NOT work as expected
'use client';
import ServerComponent from './ServerComponent'; // Will become a Client Component
export default function ClientComponent() {
return <ServerComponent />; // No longer runs on server
}
// ✅ Better - pass as children
// In parent Server Component
<ClientComponent>
<ServerComponent />
</ClientComponent>
Decision Tree: Server or Client Component?
Ask yourself these questions:
- Do I need interactivity? (clicks, inputs) → Client Component
- Do I need React state or effects? → Client Component
- Do I need browser APIs? → Client Component
- Am I just displaying data? → Server Component
- Do I need to fetch data? → Server Component (preferred)
- Do I need to access databases directly? → Server Component
Practice Exercise
Task: Create a blog post page that:
- Fetches post data from a database (Server Component)
- Displays the post content (Server Component)
- Has a "Like" button with a counter (Client Component)
- Has a comment form (Client Component)
- Displays existing comments (Server Component)
Requirements:
- Use Server Components for all data fetching
- Use Client Components only where interactivity is needed
- Pass data from Server to Client Components correctly
- Ensure proper composition of components
Bonus Challenge: Implement optimistic UI updates for the like button and comments without refetching the entire page.
Summary
- Server Components render on the server and send zero JavaScript to the client
- Client Components enable interactivity and use the 'use client' directive
- By default, all components in Next.js App Router are Server Components
- Server Components can directly access databases and server-only resources
- Client Components are needed for state, effects, event handlers, and browser APIs
- You can compose Server and Client Components by passing children or props
- Data passed between Server and Client Components must be JSON-serializable
- Use Server Components by default and opt into Client Components only when needed