React · Rendering strategies & advanced

React Server Components (RSC)

Hard ⏱ 1.5h #rsc#rendering

← React

TL;DR — Components that render only on the server, ship zero JavaScript to the client, and can touch server resources directly (DB, filesystem, secrets). They render to a serialized UI description (the "flight" payload), not HTML — and they never hydrate. 'use client' marks the boundary where interactive components take over. RSC is not SSR.


🧠 Mental model

The instinct is "server components are SSR." They're not, and conflating them is the classic miss.

  • SSR takes your client components, runs them once on the server to produce HTML, sends it, then hydrates — the same components boot up again on the client with all their JS.
  • RSC are components that exist only on the server. They produce a serialized tree (elements + references to client components), never run on the client, have no state, no effects, no event handlers, and ship no JS.

So the tree is a sandwich: server components form the static, data-fetching shell; 'use client' components are the interactive leaves. A server component can be async and await its data inline — no useEffect, no loading state, no API route.

⚙️ How it actually works

'use client' is a directive at the top of a module that marks a boundary in the import graph. Everything not behind a 'use client' door is a server component (in an RSC framework). At that boundary the bundler splits: server code stays on the server; the client module and its dependencies are code-split into a chunk.

Server components render in a server environment to the RSC payload — a streaming, JSON-like description where host elements are inlined and client components appear as references (a module id + serialized props). The client runtime receives this stream, downloads only the referenced client chunks, and reconciles the payload into the React tree — merging server output and client interactivity into one tree.

The hard rule at the boundary: props crossing server → client must be serializable. Strings, numbers, plain objects, arrays, promises, and Server Actions pass; functions, class instances, and Dates (as behaviour) do not. And you don't import a server component into a client component — instead you pass it as children/props (the "donut" pattern), so the server renders it and hands the client an already-rendered slot.

💻 Code

// Server Component (default in an RSC framework) — async, direct data access.
import { db } from '@/lib/db';           // never bundled to the client
export default async function Page() {
  const posts = await db.post.findMany(); // no fetch, no useEffect, no API route
  return (
    <main>
      {posts.map((p) => <Article key={p.id} post={p} />)}
      <LikeButton postId={posts[0].id} /> {/* a client component */}
    </main>
  );
}
// Client Component — opts INTO the bundle to be interactive.
'use client';
import { useState } from 'react';
export function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);      // state ⇒ must be client
  return <button onClick={() => setLiked((v) => !v)}>{liked ? '♥' : '♡'}</button>;
}
// ❌ Importing a server component into a client one doesn't work.
// ✅ The donut: pass server-rendered UI through as children.
'use client';
export function Panel({ children }) { /* interactive shell */ return <div>{children}</div>; }
// <Panel><ServerHeavyContent /></Panel>  ← server renders the filling

⚖️ Trade-offs

  • The win is bundle size and data locality. Markdown renderers, date libraries, ORMs, and syntax highlighters run on the server and ship 0 KB. Data access needs no API layer, and secrets never leave the server.
  • It requires a framework and bundler integration. RSC isn't something you flip on in a plain Vite SPA — it needs the App Router / a compatible bundler that understands the directives and flight protocol.
  • Client components are still SSR'd. 'use client' doesn't mean "renders only on the client" — it means "also ships to the client and hydrates." It's an addition of a runtime, not a relocation.
  • The serialization boundary constrains your API. You'll refactor to keep props serializable and to push interactivity down to small leaves. That discipline is the cost of the zero-JS default.

💣 Gotchas interviewers probe

  • "RSC is just SSR." The distinguishing answer: SSR produces HTML and hydrates; RSC produce a serialized tree and never hydrate, never ship JS. Know this cold.
  • Server components can't use useState, useEffect, event handlers, or browser APIs. The moment you need any of those, it must be a client component.
  • 'use client' marks a boundary, not a file. Everything imported into a client module becomes client code too — so put the directive as low in the tree as possible to keep the server portion large.
  • You can't import a server component into a client component. Pass it as children/props — the donut pattern — so the server still renders it.
  • Props must serialize. Passing a function (other than a Server Action), a class instance, or expecting a live Date across the boundary fails.
  • React Context doesn't reach server components. They run before/outside the client tree; provide data via props or server-side sources instead.

🎯 Say this in the interview

"Server Components render only on the server, ship no JavaScript, and can hit the database or filesystem directly — a server component can be async and await its data inline. The distinction I lead with is that this is not SSR: SSR runs your client components on the server to produce HTML and then hydrates them, whereas server components produce a serialized tree, never hydrate, and never send their code to the client. 'use client' marks the boundary where interactive leaves take over — and those client components are still SSR'd, so the directive means 'also ship to the client,' not 'client-only.' The practical constraints I keep in mind: props crossing into a client component have to be serializable, and you compose server content into client components as children rather than importing them. The payoff is big bundle savings — heavy server-only libraries ship zero bytes — and data access without an API layer."

🔗 Go deeper

Primary source — the single best place to go deeper react.dev: server components
ESC

Type to search across every question bank.

navigate open 3,610 questions indexed