React Server Components Explained Without the Hype
What Server Components actually are, the client boundary, what you can and cannot pass across it, and how to decide which components need "use client".
Table of contents
- The default is server
- The boundary is a subtree, not a file
- What can cross the boundary
- What each kind can do
- The mental shift
- Frequently asked questions
- Do Server Components replace SSR?
- Can I use a context provider in a Server Component?
- How do I do interactivity in a mostly-server page?
- Is 'use client' a performance escape hatch to avoid?
- Related reading
- References
A Server Component runs on the server and never ships to the browser. Not "runs first and then hydrates" — it does not exist on the client at all. Its output is a serialised description of UI, and its code, imports and dependencies stay on the server.
That is the whole idea, and the practical consequence is large: a component that formats dates with a 70 kB library costs 0 kB in the browser.
The default is server#
In the App Router every component is a Server Component unless it says otherwise. 'use client' marks a boundary:
// No directive → Server Component
async function ProductPage({ id }) {
// Direct data access. No API route, no useEffect, no loading state.
const product = await db.products.findById(id);
return <ProductDetails product={product} />;
}'use client'; // this file and everything it imports goes to the browser
import { useState } from 'react';
export function AddToCart({ productId }) {
const [pending, setPending] = useState(false);
return <button onClick={/* ... */}>Add to cart</button>;
}The boundary is a subtree, not a file#
This is the part that trips people up. 'use client' does not mark one component — it marks the entry point of a client subtree. Everything imported from a client component becomes client code, transitively.
The consequence: 'use client' at the top of your layout makes your whole app a client app. Push the directive down to the leaves that genuinely need interactivity.
Page (server)
├── Header (server)
│ └── ThemeToggle ('use client') ← only this ships JS
├── ProductList (server)
└── AddToCart ('use client') ← and thisWhat can cross the boundary#
Props passed from a Server to a Client Component must be serialisable:
| Works | Does not work |
|---|---|
| strings, numbers, booleans, null | functions |
| plain objects and arrays | class instances |
| Date, Map, Set, BigInt | Symbols (except registered) |
| JSX elements | anything with methods |
| Server Action references | Lucide icon components |
That last row is worth internalising. This fails:
// Server Component
const tool = { name: 'JSON Formatter', icon: FileJson }; // icon is a function
return <ToolShell tool={tool} />; // ErrorThe fix is to pass only serialisable data and let the client resolve the rest — for example by keying into a client-side map:
return <ToolShell slug={tool.slug} name={tool.name} />;You can pass a Server Component as children into a Client Component. The client component renders it as an opaque slot without ever seeing its code:
<ClientTabs>
<ServerRenderedPanel /> {/* still server-rendered */}
</ClientTabs>What each kind can do#
Server Components can: await data directly, read secrets and environment variables, use Node APIs, import heavy libraries for free.
Server Components cannot: use state or effects, attach event handlers, use browser APIs, or use context (they can read cookies() and headers() instead).
Client Components can do everything they always could, and additionally receive serialised props and Server Action references.
The mental shift#
The old model was "fetch in an effect, render, hydrate". The new one is "fetch where you render". A list page becomes:
export default async function Posts() {
const posts = await getPosts(); // no client fetch, no spinner
return posts.map((p) => <PostCard key={p.id} post={p} />);
}No loading state, no useEffect, no waterfall, and no client-side data-fetching library.
Frequently asked questions#
Do Server Components replace SSR?#
No — they complement it. SSR renders your client components to HTML for the initial load; Server Components never become client components at all. A page typically uses both.
Can I use a context provider in a Server Component?#
You cannot create one, but you can render a client provider and pass Server-rendered children through it. That is the standard pattern for theme providers.
How do I do interactivity in a mostly-server page?#
Extract the interactive part into its own small 'use client' component. The surrounding page stays server-rendered.
Is 'use client' a performance escape hatch to avoid?#
No — it is a normal part of the architecture. Interactive UI needs client code. The goal is to place the boundary precisely, not to eliminate it.