React Server Components (RSC) are components that render exclusively on the server. Unlike traditional server-side rendering, they don't send component JavaScript to the client—only the rendered output. This reduces bundle size and enables direct access to server resources like databases and file systems.
How Server Components work
When you render a Server Component:
- React executes the component on the server
- The output is serialized as RSC payload (or HTML for initial load)
- Client receives the payload and renders it
- No component code ships to the browser
// This component runs only on the server
async function ProductList() {
// Direct database access - no API needed
const products = await db.query('SELECT * FROM products');
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
The database driver and query logic never reach the client.
Server vs Client Components
| Feature | Server Component | Client Component |
|---|---|---|
| Render location | Server only | Server + client |
| JavaScript shipped | None | Component code |
| Can use hooks | No | Yes |
| Can use browser APIs | No | Yes |
| Can fetch data | Yes (async/await) | Yes (useEffect, etc.) |
| Can access server | Yes (directly) | No (needs API) |
| Can import server-only | Yes | No |
Default behavior
In Next.js App Router, all components are Server Components by default:
// app/page.tsx - Server Component (default)
export default async function Page() {
const data = await fetchData();
return <div>{data.title}</div>;
}
No directive needed. The component runs on the server.
Creating Client Components
Add "use client" at the top of a file:
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
The directive marks the file—and everything it imports—as client code.
When to use Server Components
Use Server Components for:
| Use case | Why |
|---|---|
| Data fetching | Direct database/API access without client exposure |
| Heavy computations | Process on server, send result |
| Large dependencies | Keep markdown parsers, image processors off client |
| Sensitive logic | API keys, business rules stay on server |
| Static content | No JS overhead for non-interactive content |
When to use Client Components
Use Client Components for:
| Use case | Why |
|---|---|
| Interactivity | onClick, onChange, form handling |
| React hooks | useState, useEffect, useContext |
| Browser APIs | window, localStorage, navigator |
| Third-party UI | Libraries that use hooks internally |
| Real-time updates | WebSocket subscriptions |
Composition patterns
Pattern 1: Server parent, Client child
The most common pattern—Server Component fetches data, Client Component handles interaction:
// app/products/page.tsx (Server Component)
import { ProductFilter } from './ProductFilter';
export default async function ProductsPage() {
const categories = await getCategories();
const products = await getProducts();
return (
<div>
{/* Client Component receives server data as props */}
<ProductFilter categories={categories} />
<ProductList products={products} />
</div>
);
}
// app/products/ProductFilter.tsx (Client Component)
'use client';
import { useState } from 'react';
export function ProductFilter({ categories }) {
const [selected, setSelected] = useState(null);
// Interactive filter UI
}
Pattern 2: Children as Server Components
Pass Server Component output as children to Client Components:
// Server Component
export default async function Page() {
return (
<ClientWrapper>
<ServerContent /> {/* Rendered on server, passed as children */}
</ClientWrapper>
);
}
// Client Component
'use client';
export function ClientWrapper({ children }) {
const [isOpen, setIsOpen] = useState(true);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children} {/* Server-rendered content */}
</div>
);
}
The Server Component renders before being passed. It doesn't become a Client Component.
Pattern 3: Slots for flexibility
Use props to pass multiple Server Component trees:
// Server Component
export default async function DashboardPage() {
return (
<DashboardLayout
sidebar={<Sidebar />} {/* Server Component */}
header={<Header />} {/* Server Component */}
main={<DashboardContent />} {/* Server Component */}
/>
);
}
// Client Component
'use client';
export function DashboardLayout({ sidebar, header, main }) {
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<div className="flex">
{sidebarOpen && sidebar}
<div className="flex-1">
{header}
{main}
</div>
</div>
);
}
Data fetching
Server Components can fetch data directly:
// Direct fetch
async function Posts() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return <PostList posts={posts} />;
}
// Database query
async function Users() {
const users = await db.select().from(usersTable);
return <UserList users={users} />;
}
// File system access
async function Docs() {
const content = await fs.readFile('./docs/intro.md', 'utf-8');
return <MarkdownRenderer content={content} />;
}
No useEffect, no loading states in the component—Suspense handles loading.
Server-only code
Mark code that should never run on client:
import 'server-only';
export async function getSecretData() {
const apiKey = process.env.SECRET_API_KEY;
// This throws at build time if imported in Client Component
}
Import server-only in modules that use secrets or server APIs.
Streaming and Suspense
Server Components can stream with Suspense:
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Header /> {/* Sends immediately */}
<Suspense fallback={<Skeleton />}>
<SlowDataComponent /> {/* Streams when ready */}
</Suspense>
<Footer /> {/* Sends immediately */}
</div>
);
}
The page structure sends immediately. Slow components stream in.
Common mistakes
| Mistake | Fix |
|---|---|
"use client" on everything |
Only add where needed |
| Hooks in Server Components | Move to Client Component |
| Importing Client in Server | Works fine, but creates boundary |
| Large Client Components | Split into smaller Client + Server parts |
| Prop drilling to avoid Client | Use composition patterns instead |
Bundle size impact
Server Components keep dependencies off the client:
// Server Component - these don't ship to client
import { marked } from 'marked'; // ~50KB
import { highlight } from 'prism'; // ~100KB
import { processImage } from 'sharp'; // Large native module
async function BlogPost({ slug }) {
const post = await getPost(slug);
const html = marked(post.content);
return <article dangerouslySetInnerHTML={{ __html: html }} />;
}
The client receives only the rendered HTML.
Caching and revalidation
Server Component data can be cached:
// Cached indefinitely
const data = await fetch(url);
// Revalidate every hour
const data = await fetch(url, { next: { revalidate: 3600 } });
// No caching
const data = await fetch(url, { cache: 'no-store' });
Use revalidatePath() or revalidateTag() for on-demand revalidation.
Debugging
Check where components run:
// Add to any component
console.log('Running on:', typeof window === 'undefined' ? 'server' : 'client');
In Next.js dev tools, Server Components show in the component tree without the client bundle indicator.
Summary
Server Components render on the server and ship no JavaScript. They can access databases, file systems, and server APIs directly. Use them by default for data fetching and non-interactive UI. Add "use client" only for components that need hooks or browser APIs. Compose by passing Server Component output as children or props to Client Components. The result is smaller bundles and faster page loads.
