May 2025
·3 min read
React Server Components Explained: When (and When Not) to Use Them
React Server Components run on the server, cut JS bundle size, and enable direct DB access — but they're not right for every component. Here's the complete picture.
Introduction
- React Server Components (RSC) shipped in React 18/19 and are stable in Next.js App Router.
- The fundamental shift: components can now run exclusively on the server — zero client JS emitted.
- This article clarifies the mental model, the boundaries, and the decision framework.
The RSC Mental Model
- By default in Next.js App Router, every component is a Server Component unless marked
"use client". - Server Components render to HTML + a serializable RSC payload — no hydration needed.
- Client Components still hydrate — they get the interactivity benefit with the expected JS cost.
- The tree can mix both: a Server Component can render Client Components as children (not vice versa without workarounds).
// Server Component (default — no "use client")
async function PostList() {
const posts = await db.query('SELECT * FROM posts'); // direct DB — no API needed
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
// Client Component
"use client";
import { useState } from 'react';
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(l => !l)}>{liked ? '❤️' : '🤍'}</button>;
}What Server Components Can Do That Client Components Cannot
awaitat the top level — direct database, filesystem, or external API calls.- Import server-only packages (ORMs, SDKs with secrets) without leaking them to the client.
- Zero runtime JS cost for purely presentational components.
- Access
cookies(),headers()fromnext/headers(Next.js only).
What Server Components Cannot Do
- Use React state (
useState,useReducer) or lifecycle hooks (useEffect). - Attach DOM event handlers (
onClick,onChange, etc.). - Use browser-only APIs (
window,localStorage). - Use Context providers directly (Context must be in a Client Component wrapper).
- Pass non-serializable props (functions, class instances) to Client Components.
The Component Decision Framework
Use this checklist to decide Server vs Client:
| Need | Use |
|---|---|
| Fetch data (DB, API, fs) | Server Component |
| Display-only UI, no interactivity | Server Component |
| State, events, hooks | Client Component |
| Browser APIs | Client Component |
| Authentication/cookies | Server Component |
| Animations, drag-and-drop | Client Component |
The Composition Pattern: "Server Shell, Client Leaf"
- Keep the data-fetching wrapper as a Server Component.
- Pass the fetched data down as props to a
"use client"child for interactivity. - This minimises the "client boundary" — only the interactive leaf hydrates.
// page.tsx (Server Component)
import { PostContent } from './PostContent'; // Server
import { LikeButton } from './LikeButton'; // Client
async function BlogPostPage({ slug }: { slug: string }) {
const post = await getPostBySlug(slug);
return (
<article>
<PostContent post={post} />
<LikeButton postId={post.id} /> {/* only this hydrates */}
</article>
);
}Avoiding the "Client Component Sprawl" Anti-Pattern
- Marking a parent
"use client"converts its entire subtree to Client Components. - Strategy: push
"use client"as far down the tree as possible. - Use the
childrenprop pattern to pass Server Component content into a Client wrapper without making it a client component.
Streaming and Suspense
- Server Components compose naturally with
<Suspense>and React's streaming. - Next.js
loading.tsxuses Suspense under the hood — instant shell, streamed content. use()hook (React 19) can suspend on a promise passed from a Server Component.
Common Mistakes
- Putting
"use client"at the layout level — makes the whole app a Client tree. - Trying to share component state between a Server and Client Component.
- Importing a server-only module into a Client Component (use
server-onlypackage to throw at build time if this happens). - Forgetting that
paramsandsearchParamsin Next.js are now async (await params).
Conclusion
- Default to Server Components for anything that doesn't need interactivity.
- Push
"use client"to the leaves — interactive widgets, not page wrappers. - The result: smaller JS bundles, faster Time-to-Interactive, and simpler data fetching.
- Next read: "Data Fetching in Next.js: Caching, Revalidation & Streaming."