React 89 ⚛️ React Server Components (RSC) Architecture
React Server Components are not a rendering mode. They are a new kind of component. A Server Component runs ahead of time, in an environment separate from the client application and separate from the traditional SSR server, and its code never ships to the browser . That separation is the architectural core. The component can query a database, read the filesystem, or call an internal service during its own render, and the client receives only the serialized output—not the component, not its imports, not its secrets . When a component needs interactivity, it is marked with 'use client', and that module and its transitive dependency subtree become client-evaluated code . The boundary lives on the import graph, not on the component tree.
The distinction from SSR is the one that matters most. SSR renders the full React tree to HTML on the server, but the same component code still ships to the browser to hydrate that HTML . RSC keeps the Server Component code on the server entirely. The client receives the RSC Payload—a compact, streamable representation of the rendered tree that includes references to Client Component bundles and the serialized props passed to them . Frameworks like Next.js then use that payload, together with the Client Components, to produce the initial HTML. RSC can run without SSR, and SSR can run without RSC; the mature frameworks combine both .
Key point: A Server Component renders on the server and its code never ships to the client; a Client Component renders on the server (during SSR) and also runs in the browser. The 'use client' directive creates a boundary in the module graph, not in the render tree, and every module imported below that boundary becomes client code. There is no 'use server' directive for Server Components—'use server' is for Server Functions, which are a different feature.
Why RSC exists as a separate component type
The bundle-size problem. A traditional React page that renders markdown from a CMS typically ships the markdown parser, the syntax highlighter, and the date-formatting library to every visitor, then fetches the content in a useEffect after the page loads. The React documentation measures this pattern at roughly 75 KB of gzipped libraries plus a second network request, for content that never changes for the life of the page . A Server Component renders that content on the server and sends only the output. The parser and the highlighter never reach the browser, and the content is visible on first page load .
The data-access problem. Before RSC, a component that needed data either received it as props from a framework function (getStaticProps, getServerSideProps) or fetched it on the client after mount. RSC lets the component fetch during its own render, with direct access to the database or the internal service, without building an API endpoint that exposes the data to the client first . The component is async, and await inside the render suspends the component while the data resolves .
The interactivity problem. A Server Component cannot use useState, useEffect, event handlers, or browser APIs. It is not sent to the browser, so it has no state to manage and no DOM to interact with . Interactivity is added by composing a Server Component with a Client Component. The Server Component renders the data and passes it as props to the Client Component, which handles the interaction. The Client Component receives the Server Component’s rendered output through the RSC Payload, not through a separate API call .
The environment problem. Server Components can read secrets, database credentials, and server-only modules. That capability is also a risk: a careless prop passed across the boundary is serialized and sent to the browser, visible to anyone inspecting network traffic . The architecture relies on the developer being deliberate about what crosses the boundary, and the ecosystem provides tools—the server-only package, taint APIs—to enforce that discipline at build time .
a. The module boundary and the 'use client' directive
The 'use client' directive is the only directive that marks a boundary. It is placed at the top of a file, and it declares that the module and everything it imports belong to the client module graph . There is no directive for Server Components; a component is a Server Component by default in an RSC-enabled framework unless a parent module’s 'use client' boundary reaches it .
// app/page.js
// This file is a Server Component by default.
// It can be async, and it can await data.
import { getPosts } from './data'
import { LikeButton } from './LikeButton'
export default async function Page() {
// Fetch directly on the server during render.
const posts = await getPosts()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.title}
{/* LikeButton is a Client Component.
The server passes serialized props to it. */}
<LikeButton postId={post.id} />
</li>
))}
</ul>
)
}
// app/LikeButton.js
'use client'
// The directive marks this module and its imports as client code.
import { useState } from 'react'
export function LikeButton({ postId }) {
// useState is allowed because this is a Client Component.
const [liked, setLiked] = useState(false)
return (
<button onClick={() => setLiked(!liked)}>
{liked ? 'Liked' : 'Like'}
</button>
)
}
The placement of 'use client' determines the size of the client bundle. If the directive is placed on the page module, the entire page and everything it imports become client code. If it is placed on the smallest interactive leaf, only that leaf and its dependencies ship to the browser . The rule is to place the boundary as deep as possible in the import graph, not as high as possible in the component tree.
A Client Component can still render a Server Component by receiving it as children or as a prop. The server renders the Server Component first, serializes its output into the RSC Payload, and the Client Component renders the already-created JSX . This is how a client-side modal can contain server-fetched content without making the content client-side.
b. The RSC Payload and the Flight protocol
The RSC Payload is the wire format that replaces the Server Component code on the client. It is not HTML. It is a serialized description of the Server Component tree that includes the rendered output, references to the Client Component bundles that the browser must load, and the props passed from Server Components to Client Components . React serializes this payload using the Flight protocol, which is streamable and can be interleaved with Suspense boundaries .
// The RSC Payload is not human-readable in practice,
// but conceptually it contains:
// - the rendered output of Server Components
// - references to Client Component chunks
// - serialized props for those Client Components
//
// The browser reconstructs the tree from this payload
// and renders it alongside the HTML produced by SSR.
The payload is what makes the client-side navigation fast. On a direct visit, the server renders the full tree to HTML, and the browser hydrates it. On a subsequent client-side navigation, the framework requests a new RSC Payload for the target route, and React renders the new Server Component output in the browser without a full page reload . The Server Component code still runs on the server for that navigation; the client only receives the output.
The serialization constraint is the boundary’s hard edge. Props passed from Server Components to Client Components must be serializable. Functions, class instances, and symbols cannot cross. Dates are serialized as strings. The RSC Payload is visible in network traffic, so the props are public data . This is why sensitive data must stay on the server side of the boundary and be passed only through Server Functions or handled within Server Components.
c. Async components, streaming, and Suspense
Server Components introduce async components as a first-class pattern. A Server Component can be an async function, and await inside its render suspends the component until the promise resolves . The framework streams the parts of the page that are ready and holds the suspended parts behind Suspense boundaries.
// app/page.js
import { Suspense } from 'react'
import { Note } from './Note'
import { Comments } from './Comments'
export default function Page() {
return (
<div>
{/* Note is awaited on the server; its output is ready first. */}
<Note />
{/* Comments is a slower region; it streams in after the shell. */}
<Suspense fallback={<p>Loading comments...</p>}>
<Comments />
</Suspense>
</div>
)
}
The shell is the part of the page outside any Suspense boundary. React streams the shell first, then emits the fallback for the suspended region, and later streams the finished HTML for that region along with an inline script that replaces the fallback . Nested Suspense boundaries let the page reveal in stages. The React guidance warns against wrapping the entire app in a single root Suspense boundary, because that reduces the shell to a spinner; the shell should contain the layout, chrome, and enough context for the user to orient .
A Server Component can start a promise and pass it to a Client Component without awaiting it. The Client Component reads the promise with the use API, and the nearest Suspense boundary shows its fallback while the promise is pending . The request starts on the server before the client runs, so the Client Component does not need to fetch the same data after mount. This pattern is used for lower-priority data that should not block the main content from rendering.
Complete Example Session
The following session builds a product page that uses a Server Component for the product data, a Client Component for the interactive selector, and a streaming Suspense boundary for the reviews.
// ============================================
// PART 1: THE SERVER COMPONENT PAGE
// ============================================
// app/product/[id]/page.js
import { Suspense } from 'react'
import { db } from '@/lib/db'
import { VariantSelector } from './VariantSelector'
import { Reviews } from './Reviews'
export default async function ProductPage({ params }) {
// Await the route param in Next.js 15+.
const { id } = await params
// Query the database directly during render.
const product = await db.products.findFirst({ where: { id } })
if (!product) {
// notFound is a framework helper that renders a 404.
notFound()
}
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* The selector is the only interactive part.
The product data is serialized as props. */}
<VariantSelector variants={product.variants} />
{/* Reviews are slower; stream them after the shell. */}
<Suspense fallback={<p>Loading reviews...</p>}>
<Reviews productId={product.id} />
</Suspense>
</main>
)
}
// ============================================
// PART 2: THE CLIENT COMPONENT
// ============================================
// app/product/[id]/VariantSelector.js
'use client'
import { useState } from 'react'
export function VariantSelector({ variants }) {
// The state is client-only.
const [selected, setSelected] = useState(variants[0].id)
return (
<select value={selected} onChange={(e) => setSelected(e.target.value)}>
{variants.map((variant) => (
<option key={variant.id} value={variant.id}>
{variant.label}
</option>
))}
</select>
)
}
// ============================================
// PART 3: THE STREAMED SERVER COMPONENT
// ============================================
// app/product/[id]/Reviews.js
import { db } from '@/lib/db'
export async function Reviews({ productId }) {
// This await suspends the Reviews component,
// but not the product shell above it.
const reviews = await db.reviews.findMany({ where: { productId } })
return (
<ul>
{reviews.map((review) => (
<li key={review.id}>{review.text}</li>
))}
</ul>
)
}
// ============================================
// PART 4: THE RSC PAYLOAD (CONCEPTUAL)
// ============================================
// The server sends a payload that includes:
// - the rendered <h1>, <p> from the product data
// - a reference to the VariantSelector client chunk
// - the serialized variants prop
// - a placeholder for the Reviews Suspense boundary
// The browser renders the shell immediately,
// loads the VariantSelector chunk, and streams Reviews later.
// ============================================
// PART 5: THE CLIENT-SIDE NAVIGATION
// ============================================
// Navigating from /product/1 to /product/2
// requests a new RSC payload for the target route.
// The server runs the ProductPage and Reviews
// Server Components again, and the client renders
// the new output without a full page reload.
The four parts cover the Server Component page with a direct database query, the Client Component with its boundary and state, the streamed Server Component behind Suspense, and the conceptual payload and navigation flow. What the session demonstrates is that the boundary is the directive, the data flows from server to client as serialized props, and the slow region streams without blocking the shell.
Quick Reference
Server vs Client Components
| Aspect | Server Component | Client Component |
|---|---|---|
| Runs on server | Yes | Yes (during SSR) |
| Runs in browser | No | Yes |
| Ships to client | No | Yes |
Uses useState | No | Yes |
Uses useEffect | No | Yes |
| Uses browser APIs | No | Yes |
Can be async | Yes | No |
| Accesses database | Yes | No |
| Directive | None (default) | 'use client' |
Directives
| Directive | Purpose |
|---|---|
'use client' | Mark module boundary for client |
'use server' | Mark Server Function (not component) |
| No directive | Server Component by default |
Boundary Rules
| Rule | Consequence |
|---|---|
'use client' is on the import graph | All imports below it become client code |
| Boundary does not follow render tree | Client Component can render Server Component via children |
| Props across boundary must be serializable | Functions, class instances cannot cross |
RSC and SSR
| Aspect | RSC | SSR |
|---|---|---|
| Sends code to client | No (Server Components) | Yes (full tree) |
| Output format | RSC Payload | HTML |
| Hydration | Only Client Components | Full tree |
| Can run without the other | Yes | Yes |
| Framework combination | Next.js App Router | Next.js Pages Router |
Best Practices
✅ Do This:
// Place 'use client' on the smallest interactive leaf
'use client'
export function LikeButton({ postId }) { /* ... */ }
// Pass Server Components as children to Client Components
<ClientModal><ServerContent /></ClientModal>
// Use Suspense for slow Server Components
<Suspense fallback={<Spinner />}><SlowReviews /></Suspense>
❌ Don’t Do This:
// Place 'use client' on the page module
'use client' // entire page becomes client code
// Pass a function across the boundary
<ClientComp onClick={() => {}} /> // not serializable
// Pass a database secret as a prop
<ClientComp apiKey={process.env.API_KEY} /> // exposed to browser
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Entire page ships to client | 'use client' placed too high | Move to the interactive leaf |
| Function prop rejected | Props must be serializable | Pass data, not callbacks |
| Secret leaked to browser | Prop serialized into payload | Keep secrets in Server Components |
| Server Component imported in Client | Client module imports Server module | Pass as children from a Server parent |
'use server' used for component | Confused with Server Functions | Use no directive for Server Components |
Real-World Examples
1. Server Component with database query
export default async function Page() {
const posts = await db.posts.findMany()
return <List posts={posts} />
}
2. Client Component boundary
'use client'
export function Counter() { const [n, setN] = useState(0); /* ... */ }
3. Pass Server Component as children
<ClientModal><ServerCart /></ClientModal>
4. Stream slow region
<Suspense fallback={<Spinner />}><Reviews /></Suspense>
5. Async Server Component
export default async function Page() { const data = await fetch(); /* ... */ }
6. Client-side navigation payload
Navigating to /about requests a new RSC payload, not a full HTML page.
7. server-only guard
import 'server-only' // build error if imported into client
8. use in Client Component
const data = use(promisePassedFromServer)
9. Taint a sensitive object
taintObjectReference('Do not pass to client', user)
10. Shell first, slow region later
<Layout><Suspense fallback={<Skeleton />}><Feed /></Suspense></Layout>
Visual
The Module Boundary
┌──────────────────────────────────────────────┐
│ SERVER MODULE GRAPH │
│ ┌────────────────────────────────────────┐ │
│ │ Page (Server Component) │ │
│ │ imports → db, helpers │ │
│ │ imports → Client Component reference │ │
│ └────────────────┬───────────────────────┘ │
│ │ 'use client' │
│ ▼ │
│ CLIENT MODULE GRAPH │
│ ┌────────────────────────────────────────┐ │
│ │ LikeButton (Client Component) │ │
│ │ imports → useState, event handlers │ │
│ │ ships to browser │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
RSC Payload
┌──────────────────────────────────────────────┐
│ RSC Payload (serialized) │
│ ├── rendered Server Component output │
│ ├── reference to LikeButton chunk │
│ └── serialized props { postId: 1 } │
│ │ │
│ ▼ │
│ Browser reconstructs the tree, │
│ loads the chunk, renders the output. │
└──────────────────────────────────────────────┘
Streaming with Suspense
┌──────────────────────────────────────────────┐
│ Shell (layout, header, product data) │
│ │ │
│ ▼ │
│ Stream shell to browser │
│ │ │
│ ▼ │
│ Suspense fallback for Reviews │
│ │ │
│ ▼ │
│ Reviews HTML streams in, replaces fallback │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Server Component | Renders on server, no client code |
| Client Component | Renders on server + client, ships to browser |
| Client directive | 'use client' |
| Server directive | None (default) |
| RSC Payload | Serialized tree + client references + props |
| Boundary location | Module import graph |
| Async components | Server Components only |
| Streaming | Suspense-aware |
| Framework | Next.js App Router (mature) |
Key takeaways:
- A Server Component is a component type, not a rendering mode. It runs ahead of time in a separate environment, and its code never ships to the browser .
'use client'creates a boundary in the module graph, not the render tree. Every import below the directive becomes client code, which is why the directive belongs on the smallest interactive leaf .- The RSC Payload replaces the Server Component code on the client. It carries the rendered output, references to Client Component chunks, and serialized props .
- Server Components can be async and can access backend resources directly. The
awaitsuspends the component, not the rest of the page, when paired with Suspense . - There is no
'use server'directive for Server Components. That directive is for Server Functions, a separate feature . - Props across the boundary must be serializable. Functions, class instances, and symbols cannot cross, and the serialized props are visible to anyone inspecting network traffic .
- RSC and SSR are separate. RSC keeps component code on the server; SSR renders the tree to HTML. Mature frameworks combine both .
Remember: React Server Components change the unit of composition from the component tree to the module graph. A Server Component is code that never leaves the server; a Client Component is code that runs in both places. The 'use client' directive is the line between them, and the RSC Payload is what crosses it. The architecture is not a replacement for SSR or CSR—it is a finer-grained way to decide, per component, where the work should happen. The payoff is a smaller client bundle and direct data access; the cost is a new mental model for the boundary and a discipline about what data crosses it.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!