RSC Architecture
React Server Components (RSC) represent the biggest architectural shift in React since hooks. They introduce a dual-component model where your application spans two distinct environments: the Server and the Client.
[!NOTE] The Mental Model: Think of your application like a Restaurant.
- The Server (Kitchen): Where the raw ingredients (database) are. It’s private, has direct access to everything, but customers (users) can’t go there.
- The Client (Dining Room): Where the food is served. It’s interactive, public, but you can’t cook (access the database) there.
- Waiters (RSC Payload): They bring the prepared food (serialized data/UI) from the Kitchen to the Dining Room.
This isn’t just about performance; it’s about access. Server Components have direct access to your backend (databases, files, internal services), while Client Components have access to the browser (window, interactivity, state).
1. The Dual-Component Model
In the traditional “Single Page App” (SPA) or even standard SSR, all component code eventually runs on the client. With RSC, we split the world in two.
1. Server Components (The Default)
- Where they run: Exclusively on the Server.
- Output: Serialized JSON (RSC Payload), which becomes HTML.
- Bundle Size: Zero. The code for these components is never sent to the browser.
- Capabilities: Async/await, direct DB access, secret keys.
- Limitations: No
useState,useEffect,onClick, or browser APIs.
2. Client Components (The Interactive Islands)
- Where they run: Pre-rendered on Server (HTML), then Hydrated on Client.
- Output: HTML + JavaScript bundle.
- Bundle Size: Adds to the download.
- Capabilities: State, Effects, Event Listeners, Browser APIs.
- Limitations: No direct DB access, cannot be async (yet).
Interactive Architecture Explorer
Toggle between the Legacy SSR model and the RSC Architecture to see how data and code flow differently. Notice how in RSC, the “Server” components stay on the server, reducing the bundle size sent to the client.
2. The RSC Payload (The Wire Format)
When you navigate in a Next.js App Router application, the server doesn’t send HTML. Instead, it streams a special format called the RSC Payload.
This payload is a compact, line-by-line JSON representation of your component tree.
What’s inside the payload?
- Rendered Server Components: The actual HTML output (e.g.,
<div><h1>Hello</h1></div>). - Client Component References: Placeholders telling the browser “Load the
LikeButtonbundle here”. - Props: Any data passed from Server to Client components.
[!IMPORTANT] Serialization Boundary: Because the RSC Payload must be sent over the network as text, any props passed from a Server Component to a Client Component must be serializable.
- ✅ Strings, Numbers, Booleans, Null/Undefined
- ✅ Arrays, Objects (containing serializable values)
- ✅ Dates (converted to ISO string)
- ❌ Functions (e.g., event handlers)
- ❌ Classes
- ❌ DOM Elements
RSC Payload Decoder
Click on the components in the tree to see how they are represented in the RSC Payload.
Component Tree
RSC Wire Format
3. Composition: The “Hole” Pattern
A common confusion is how to mix Server and Client components.
The Rule: You cannot import a Server Component into a Client Component.
The Fix: You can pass a Server Component as a prop (usually children) to a Client Component.
The “Donut” Analogy
Think of your Client Component as a Donut. The interactive part (state, effects) is the dough. The empty space in the middle is the children prop.
You can put anything inside that hole—even a Server Component! The Client Component doesn’t need to know what’s in the hole; it just renders the space for it.
// ❌ WRONG: Importing Server Component into Client Component
'use client';
import ServerComponent from './ServerComponent'; // Error: Server Component in Client Module
export default function ClientWrapper() {
return (
<div>
<ServerComponent />
</div>
);
}
// ✅ CORRECT: Passing as Children
// client-wrapper.tsx
'use client';
export default function ClientWrapper({ children }: { children: React.ReactNode }) {
return (
<div className="border p-4">
<h2>I am Client Side (The Donut)</h2>
{children} {/* The Hole */}
</div>
);
}
// page.tsx (Server Component)
import ClientWrapper from './client-wrapper';
import ServerComponent from './server-component';
export default function Page() {
return (
<ClientWrapper>
<ServerComponent /> {/* Filling the hole on the Server */}
</ClientWrapper>
);
}
This pattern is essential for Context Providers (which must be Client Components) that wrap your entire application.
// app/providers.tsx
'use client';
import { ThemeProvider } from 'next-themes';
export function Providers({ children }) {
return <ThemeProvider>{children}</ThemeProvider>;
}
By using this pattern, you keep the majority of your tree as Server Components, pushing the Client boundary down to only where interactivity is strictly needed.