React Server Components: What They Actually Are
RSC is not SSR 2.0 — it's a fundamentally different model. Here's what React Server Components actually are, why they exist, and how the React Flight Protocol works.
React Server Components (RSC) are the most misunderstood feature in React 19. The confusion is almost always the same: people conflate them with server-side rendering. They are different ideas that happen to both involve a server.
RSC vs SSR: The Difference
| SSR | RSC | |
|---|---|---|
| When it runs | Once per request, before sending HTML | Always — component lives on the server |
| Client receives | HTML string + full React app for hydration | Rendered output only — never the component code |
| Server involvement | One request/response cycle | Ongoing — server component stays server-side |
| Can query DB | Technically, but messy | Yes, cleanly — credentials never leave server |
| Interactive state | Yes (after hydration) | No — must use a client component |
You can have SSR without RSC. You can have RSC without SSR. Next.js uses both together, which is why they look like the same thing — they're not.
What an RSC Is
A React Server Component is a component that only ever runs on the server. Not "server-side rendered once and then hydrated" — actually only ever on the server, permanently.
Three consequences:
- The client bundle never includes the component code. Users don't download it, and it can't be inspected in DevTools.
- The component can use
async/awaitdirectly. Server components are async functions. You canawaita database query right inside the component. - The component cannot use browser APIs,
useState,useEffect, or any hook that requires client-side execution.
// ServerComponent.jsx
// ✅ This is valid — async server component querying a DB
export default async function NoteList() {
// This query runs on the server — credentials never leave
const notes = await db.all('SELECT id, body, created_at FROM notes');
return (
<ul>
{notes.map(note => (
<li key={note.id}>{note.body}</li>
))}
</ul>
);
}// ClientComponent.jsx
"use client"; // ← this directive opts the component in to client rendering
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}By default in an RSC framework, everything is a server component. You use "use client" to explicitly opt into client rendering.
Why RSCs Exist
Three problems they solve:
1. The waterfall problem. In a traditional SPA, the client fetches data after the component mounts. For nested components, this creates request waterfalls: parent fetches → parent renders → child mounts → child fetches. Server components can fetch data in parallel at render time.
2. The bundle size problem. Heavy libraries — markdown parsers, syntax highlighters, date formatters — can live entirely in server components. Users download zero bytes of them.
3. The secret credentials problem. Connection strings, API keys, and private credentials have to stay on the server. In a traditional SPA, you need an API layer to proxy every request through the server. RSCs eliminate the proxy — the component itself is the proxy.
The React Flight Protocol
When a server component renders, it doesn't send HTML (that's SSR). It sends a special serialization format called the React Flight Protocol — a streaming, JSON-like wire format that describes the component tree.
The client receives something like this (simplified):
J0:["$","ul",null,{"children":[
["$","li",{"key":"1"},{"children":"First note"}],
["$","li",{"key":"2"},{"children":"Second note"}]
]}]The client's RSC runtime (react-server-dom-webpack/client) reads this stream and reconstructs the React element tree without needing the original component code.
// client.js
import { createRoot } from 'react-dom/client';
import { createFromFetch } from 'react-server-dom-webpack/client';
const root = createRoot(document.getElementById('root'));
// Fetch the Flight stream from the RSC server endpoint
const componentTree = createFromFetch(fetch('/rsc'));
root.render(componentTree);The server endpoint doesn't return HTML — it returns the Flight stream:
// server/rsc-endpoint.js
import { renderToPipeableStream } from 'react-server-dom-webpack/server';
import App from '../src/App.jsx';
app.get('/rsc', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream');
const { pipe } = renderToPipeableStream(<App />);
pipe(res);
});The Component Boundary Rule
The one constraint that trips up every RSC beginner: a server component cannot be a direct child of a client component.
// ❌ This fails — RSC can't be a child of "use client" component
"use client";
function ClientWrapper() {
return <ServerComponent />; // ← server component inside client component
}The reason: once the client component renders on the client, the server component's code isn't there. The client can't call it.
The workaround: pass the server component as children from a parent server component.
// ✅ Works — server component passed as prop from a server context
// ParentServerComponent.jsx (server)
import ClientWrapper from './ClientWrapper';
import ServerComponent from './ServerComponent';
export default function ParentServerComponent() {
return (
<ClientWrapper>
<ServerComponent /> {/* pre-rendered on server, passed as a prop */}
</ClientWrapper>
);
}
// ClientWrapper.jsx (client)
"use client";
export default function ClientWrapper({ children }) {
const [open, setOpen] = useState(false);
return <div>{open && children}</div>;
}This works because children is the already-rendered Flight output from the server — the client component never needs to call ServerComponent itself.
RSC Without a Framework: Not Recommended
Wiring up RSC without Next.js requires configuring Webpack with react-server-dom-webpack, patching the Node module system to handle JSX server-side, running a separate RSC server alongside the regular web server, and managing the Flight protocol streaming yourself.
It's a useful exercise for understanding what frameworks abstract away. For production, use Next.js, Remix, or TanStack Start — they handle all of this correctly.
Practice what you just read.
Keep reading
Enjoyed this? Get more like it.
Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.