Server vs Client Components: Deep Dive
The line between server and client components is the most important architectural decision in a React Server Components application. Server components are async, can access databases, but cannot use hooks or browser APIs. Client components are synchronous, can use all React hooks and browser APIs, but cannot directly access your backend secrets or databases. Understanding what each can and cannot do—and how to compose them—is essential to building correct, performant applications.
This article gives you the complete capabilities matrix, shows you how to reason about the boundary, and walks through real composition patterns. By the end, you will never again misplace a 'use client' directive or pass a server-side value across the boundary incorrectly.
Capabilities Matrix: Server vs. Client Components
Server components and client components are fundamentally different runtime environments. The following table lists the core capabilities and constraints:
| Capability | Server Component | Client Component |
|---|---|---|
| Async/await (native) | Yes | No |
| Access database directly | Yes | No |
| Access environment variables | Yes | No |
Use useState, useEffect, etc. | No | Yes |
| Use browser APIs (localStorage, fetch) | No | Yes |
| Render as a child of a client component | No | Yes |
| Pass non-serializable data to children | No | Yes (within client boundary) |
| Accept serializable props from server | Yes | Yes |
| Be a child of a server component | Yes | Yes |
A server component is a coroutine: it can await promises directly in the component body. This is the fundamental difference. In traditional React (and client components), you cannot write const data = await fetch(...) in the component body. You must use useEffect or a separate data-fetching function. Server components lift this restriction because they execute entirely on the server.
// Server component: direct async/await
async function BlogPosts() {
const posts = await db.posts.findAll();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// Client component: useEffect for data fetching
'use client';
import { useEffect, useState } from 'react';
export function BlogPostsClient() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => setPosts(data));
}, []);
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
The server component is simpler: no hooks, no state management, no lifecycle. The client component is more complex but necessary when you need browser interactivity or real-time updates.
When to Use Server Components
Use server components for:
- Data fetching from databases or internal APIs. If you need to read from a database or call a backend service, do it in a server component. The data stays on the server; the client receives only the rendered output.
- Accessing secrets and environment variables. API keys, database credentials, and other sensitive values can be accessed directly in server components without exposure.
- Large dependencies. If your component relies on a large library (e.g., a markdown parser, ORM, or data-processing library), keep it on the server so it doesn't inflate the client bundle.
- Rendering static or server-only content. Layouts, header navigation, footer—anything that doesn't need browser interactivity—can be a server component.
// Server component: fetches a user profile and renders static HTML
async function UserProfile({ userId }) {
const user = await db.users.findById(userId);
const posts = await db.posts.findByAuthor(userId);
return (
<div className="profile">
<h1>{user.name}</h1>
<p>{user.bio}</p>
<h2>Posts</h2>
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={`/posts/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}
export default UserProfile;
When to Use Client Components
Use client components for:
- Interactivity. Any button, form, or interaction that responds to user input must be a client component.
- Real-time updates. WebSockets, polling, or subscription-based data require a client component and browser runtime.
- Browser APIs. localStorage, geolocation, the DOM API, or any browser-specific functionality requires a client component.
- React hooks. If you need
useState,useEffect,useContext, or custom hooks, you must use a client component.
// Client component: form submission and real-time validation
'use client';
import { useState } from 'react';
export function ContactForm() {
const [formData, setFormData] = useState({ name: '', email: '' });
const [submitted, setSubmitted] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
const res = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(formData),
});
if (res.ok) setSubmitted(true);
};
return (
<form onSubmit={handleSubmit}>
<input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Name"
/>
<input
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="Email"
/>
<button type="submit">Send</button>
{submitted && <p>Thank you!</p>}
</form>
);
}
Composition Patterns: Server Components with Client Children
The most important rule: a server component can contain a client component, but a client component cannot directly render a server component.
This is because a server component must execute on the server. If you place a server component inside a client component, the server has no way to run it during the client-side render. The boundary flows in one direction: server to client.
Correct pattern:
// app/page.tsx — server component
import { ClientSearch } from './client-search';
async function HomePage() {
const posts = await db.posts.findAll();
return (
<div>
<h1>Blog</h1>
<ClientSearch />
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
export default HomePage;
// components/client-search.tsx — client component
'use client';
import { useState } from 'react';
export function ClientSearch() {
const [query, setQuery] = useState('');
const handleSearch = async () => {
const res = await fetch(`/api/search?q=${query}`);
const results = await res.json();
// Handle results...
};
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search posts..."
/>
<button onClick={handleSearch}>Search</button>
</div>
);
}
The server component (HomePage) renders the static blog post list and embeds the client component (ClientSearch). The server executes, fetches the posts, and generates the RSC Payload with the posts data and a reference to ClientSearch. The browser hydrates ClientSearch and uses it for interactivity.
Incorrect pattern (will not work):
// ❌ This does not work
'use client';
import { ServerComponent } from './server-component';
export function ClientParent() {
return (
<div>
<ServerComponent /> {/* Error: cannot render server component from client */}
</div>
);
}
If you try to import and render a server component from a client component, Next.js will error. The server component code cannot execute in the browser.
The "Children as Props" Pattern
To work around the one-directional boundary, use the children pattern: pass a client component as a child (via props) to a server component.
// app/layout.tsx — server component
import { ClientThemeProvider } from './client-theme-provider';
export default function RootLayout({ children }) {
// children is a server component that was passed from the router
return (
<html>
<body>
<ClientThemeProvider>
{children}
</ClientThemeProvider>
</body>
</html>
);
}
// components/client-theme-provider.tsx — client component wrapper
'use client';
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function ClientThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
This pattern is powerful: the server component structure is preserved, and client interactivity is injected where needed. The router passes the children (server components) to the layout, the layout passes them to ClientThemeProvider (a client component), which wraps them with context and interactivity.
Key Takeaways
- Server components execute on the server, have access to databases and secrets, support async/await, but cannot use hooks or browser APIs.
- Client components run in the browser, support all React hooks and browser APIs, but cannot access server secrets or execute async code directly.
- A server component can render a client component as a child; a client component cannot render a server component.
- Use server components for data fetching, static rendering, and large dependencies; use client components for interactivity, hooks, and browser APIs.
- The children-as-props pattern allows you to inject client interactivity into server-driven layouts and structures.
Frequently Asked Questions
Can I use useContext in a server component?
No. useContext is a React hook and cannot be used in server components. To share data across server components, pass props or use a shared async utility function. For client-side context, use a client component wrapper (see the children pattern above).
What happens if I try to access window in a server component?
Next.js will error at build time or runtime, telling you that window is not defined on the server. If you need the window object, move the code into a client component or use a dynamic import with ssr: false.
Can a server component import a file that has 'use client'?
Yes. A server component can import a client component; it simply renders it as a child component. The 'use client' boundary applies to that file and its imports, but the server component can still reference it.
How do I pass data from a server component to a client component?
Serialize the data as props. Pass only JSON-serializable values (strings, numbers, booleans, arrays, objects). Non-serializable values (functions, classes, symbols, Dates) cannot cross the boundary. See the next article for details on serialization.
What if I need to share logic between server and client components?
Extract the logic into a utility function or a separate module. If the logic depends on server-only resources (database, secrets), keep it in a server-only utility or use an API route. For shared business logic, use a pure function in a shared file that both can import.