Skip to main content

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:

CapabilityServer ComponentClient Component
Async/await (native)YesNo
Access database directlyYesNo
Access environment variablesYesNo
Use useState, useEffect, etc.NoYes
Use browser APIs (localStorage, fetch)NoYes
Render as a child of a client componentNoYes
Pass non-serializable data to childrenNoYes (within client boundary)
Accept serializable props from serverYesYes
Be a child of a server componentYesYes

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:

  1. 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.
  2. Accessing secrets and environment variables. API keys, database credentials, and other sensitive values can be accessed directly in server components without exposure.
  3. 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.
  4. 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:

  1. Interactivity. Any button, form, or interaction that responds to user input must be a client component.
  2. Real-time updates. WebSockets, polling, or subscription-based data require a client component and browser runtime.
  3. Browser APIs. localStorage, geolocation, the DOM API, or any browser-specific functionality requires a client component.
  4. 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.

Further Reading