If you have built anything with React in the last couple of years, you have probably heard the phrase “React Server Components” — and felt a little unsure what it actually means. You are not alone. React Server Components, explained in plain English: they are components that render on the server instead of in the browser, which lets you fetch data directly, keep heavy libraries out of your JavaScript bundle, and ship faster pages. This guide breaks down how they work, how they differ from the components you already write, and exactly when to reach for each.
What React Server Components actually are
A React Server Component (RSC) is a component that renders ahead of time, in an environment separate from your client app — either once at build time on your CI server, or fresh on every request via a web server. That separate environment is the “server” in the name.
The key consequence: the component’s code never reaches the browser. The client never sees the original component, its imports, or the data it touched. Only the rendered output crosses the wire.
This matters because of how much JavaScript you currently ship for things that do not need it. The official React docs give a concrete example: rendering markdown on the client requires the marked library (35.9K, 11.2K gzipped) and sanitize-html (206K, 63.3K gzipped) — roughly 75K gzipped of libraries — plus a second request to an API to fetch the content after the page loads. Move that component to the server and all of that disappears from the bundle. The client only receives the finished HTML.
Server Components are stable in React 19. One caveat from the React team worth knowing: the underlying bundler APIs used to implement RSC support do not follow semver and may break between React 19.x minors, so framework authors are advised to pin versions — but the component model itself is stable and will not break between minors.
Server Components vs Client Components
Client Components are what you have always written: components that run in the browser. In frameworks like Next.js’s App Router, every component is a Server Component unless you explicitly opt out with the "use client" directive at the top of the file.
| Server Components | Client Components | |
|---|---|---|
| Where they run | On the server (build time or per request) | In the browser |
| JavaScript shipped | Zero — only rendered output crosses the wire | Full component code in the bundle |
| Can use hooks | No (useState, useEffect are off limits) |
Yes |
| Can touch the database | Yes, directly | No — needs an API route |
| Can use browser APIs | No (window, localStorage) |
Yes |
Can be async |
Yes — await right in the component |
No |
| Can keep secrets | Yes — API keys never leave the server | No |
The three rules that actually matter
Forget the diagrams. If you internalise these three rules, the rest of RSC clicks into place.
1. Server Components cannot be interactive
Because Server Components never run in the browser, they cannot use anything interactive: no useState, no useEffect, no event handlers, no window or localStorage. To add interactivity, you compose: the Server Component imports and renders a Client Component marked with "use client", which owns the state and the handlers.
2. Only serialisable values cross the boundary
The server does not send HTML or JavaScript. It sends the RSC payload — a special serialised description of the rendered UI tree — which React on the client uses to build or update its component tree. This has one sharp consequence: functions cannot cross from a Server Component to a Client Component. Functions are not serialisable, so passing an event handler down as a prop from server to client throws an error. Only plain values — strings, numbers, objects, arrays, React elements — can cross.
3. There is no "use server" directive for components
This trips up almost everyone. In an RSC framework, server is the default — you opt out with "use client", you never opt in. The "use server" directive exists, but it is for Server Functions, a different feature. The React docs call this out explicitly because the misunderstanding is so common.
How this looks in a real Next.js app
Here is the pattern you will use constantly: a Server Component fetches data and renders a Client Component for the interactive parts.
// app/posts/page.tsx — Server Component (default, no directive needed)
import { db } from '@/lib/db';
import { LikeButton } from './like-button';
export default async function PostsPage() {
// Direct database access. No API route. No useEffect. No loading state.
const posts = await db.post.findMany();
return (
<main>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
{/* Client Component handles the interactivity */}
<LikeButton postId={post.id} initialLikes={post.likes} />
</article>
))}
</main>
);
}
// app/posts/like-button.tsx — Client Component
'use client';
import { useState } from 'react';
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes);
return (
<button onClick={() => setLikes(likes + 1)}>
Like ({likes})
</button>
);
}
Notice what disappeared: the /api/posts route, the client-side fetch, the loading spinner, the error state. The component itself makes the database call and renders. The browser receives a fully rendered page immediately — no layout shift as data arrives.
Push "use client" as far down the tree as possible. Every component above the boundary stays on the server and ships zero JavaScript. The boundary is a decision you make deliberately at each component.
Data fetching without the API middleman
The old pattern for dynamic data was painful: fetch in a useEffect, hit an API route, which hit the database — a waterfall of requests, with child components triggering their own fetches after the parent rendered. Server Components collapse that stack. Because components can be async, you await directly in the component body, and React suspends rendering until the promise resolves.
Streaming makes this even better. Wrap a slow section in <Suspense> and React streams the RSC payload progressively — fast sections appear first while slower data finishes, improving perceived performance without skeleton-screen gymnastics.
You can also split the work across the boundary: start a promise on the server, pass it down, and await it on the client with the use() API. The note content renders immediately while the comments below the fold load when ready.
// Server Component: start the promise, don't await it
async function Page({ id }: { id: string }) {
const note = await db.notes.get(id); // awaited: needed now
const commentsPromise = db.comments.get(id); // started: needed later
return (
<div>
<h1>{note.title}</h1>
<Suspense fallback={<p>Loading comments...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</div>
);
}
When to use which: a decision guide
Default to Server Components when the component:
- Fetches data (pages, lists, dashboards)
- Renders static content (blog posts, docs, marketing pages)
- Uses heavy libraries (markdown, syntax highlighting, PDF, date libraries)
- Needs backend access (databases, the filesystem, secrets and API keys)
- Must be SEO-friendly and fast on first load
Reach for Client Components ("use client") when the component:
- Needs interactivity:
useState, event listeners, forms - Uses effects or lifecycle:
useEffect, subscriptions, timers - Touches browser-only APIs:
window,localStorage, geolocation - Depends on client-only libraries (most UI component libraries — wrap them in your own Client Component first)
The mental model from the React team: Server Components give you the simple request/response model of old-school server-rendered apps, combined with the seamless interactivity of single-page apps. Best of both worlds — if you respect the boundary.
Three gotchas that bite everyone
1. Environment poisoning
Modules can be shared between server and client code, so it is easy to accidentally import server-only code — a database client, an API key — into a Client Component. In Next.js, only variables prefixed with NEXT_PUBLIC_ reach the client bundle; everything else is replaced with an empty string, so the code silently breaks. The fix: add import 'server-only' (via the server-only npm package) at the top of any module that must never run on the client. Importing that module from a Client Component then throws a build-time error instead of leaking at runtime.
2. You cannot import a Server Component directly into a Client Component
If a Client Component imports a Server Component in the usual way, the boundary rules break down. The supported pattern is to receive Server Components through the children prop instead: the Server Component renders the Client Component and passes the server-rendered content in as children. Learn this pattern early — it is how layouts and slots compose in the App Router.
3. Context providers need a Client Component wrapper
Server Components cannot create context, but they can render a provider — as long as the context is created in a file with the "use client" directive. So your ThemeProvider or UserContext lives in a small Client Component file, and your Server Component layout imports and renders it with a value fetched on the server. Client Components inside can then read it with useContext or the use() API.
The bottom line
If you are on the Next.js App Router, you are already writing Server Components — every file is one by default. The skill is not learning a new API so much as learning where the boundary sits: keep data and rendering on the server, push "use client" down to the interactive leaves, and never let functions or secrets cross the wire. Do that, and you get smaller bundles, faster first loads, and simpler data fetching — without giving up the interactivity that made React worth using in the first place.
Last verified: 2 October 2026, against the React 19 documentation and Next.js App Router documentation.
Further Reading & References
- Server Components – React docs (official reference: build-time vs per-request rendering, async components, the
"use client"boundary) - Server and Client Components – Next.js docs (official guide: third-party component wrapping, the
server-onlypackage, preventing environment poisoning) - Building React Apps with Server Components (2026) – practical App Router walkthrough with the core composition rules
- How React Works (Part 8): Server Components & Hydration – The Real Story – deep dive into the RSC payload format and what actually crosses the wire
- React Server Components: What Designers Need to Know – clear one-sentence mental model and UX implications on DEV
- React Server Components knowledge base – condensed reference with
asyncdata-fetching and"use client"boundary examples



