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.

April 4, 20264 min read1 / 2

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

SSRRSC
When it runsOnce per request, before sending HTMLAlways — component lives on the server
Client receivesHTML string + full React app for hydrationRendered output only — never the component code
Server involvementOne request/response cycleOngoing — server component stays server-side
Can query DBTechnically, but messyYes, cleanly — credentials never leave server
Interactive stateYes (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:

  1. The client bundle never includes the component code. Users don't download it, and it can't be inspected in DevTools.
  2. The component can use async/await directly. Server components are async functions. You can await a database query right inside the component.
  3. The component cannot use browser APIs, useState, useEffect, or any hook that requires client-side execution.
JSX
// 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> ); }
JSX
// 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):

Plain text
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.

JavaScript
// 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:

JavaScript
// 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.

JSX
// ❌ 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.

JSX
// ✅ 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.

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.

Quiz: React Server Components
1 exercise

Enjoyed this? Get more like it.

Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.