Guide · Frontend
React Server Components Explained: What Runs Where and What Ships
22 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- React Server Components are React components that run only on a server or at build time. The browser gets their finished output, never their code.
- Client Components run in the browser and are usually the interactive parts. Writing 'use client' at the top of a file tells the bundler, the build tool, to ship that file and its imports.
- Everything that crosses from server to browser must be serialisable, which means it can be turned into text and rebuilt. Plain data, dates and promises can cross. Ordinary functions and class objects cannot.
- Server Functions, marked with 'use server', let the browser call server code over the network. Their arguments come from the browser, so treat them as untrusted input.
- Beginners can stop after the section on SSR. Engineers get the real RSC payload, the text stream React calls Flight, captured on React 19.3.1, plus three tested ways secrets leak.
What goes wrong when a React page ships everything to the browser?
The browser downloads code for parts of the page that never change, then waits again to fetch their data. Phones on slow networks feel both delays. React Server Components are React's answer to that waste.
Picture a common setup. An online shop has a product page for a steel water bottle priced at Rs 349. The page shows a title, a description written in Markdown, a plain-text formatting style, a list of reviews and one Like button. The setup is made up, but the problem is real.
In a classic React app, every one of those components is JavaScript the browser must download. The Markdown library comes too, even though the description never changes after it loads. Then the page asks an API for the product data and renders a spinner while it waits.
Only the Like button actually needs to run in the browser. Everything else could be prepared on the server, close to the data. This guide follows that one product page from the database to the screen.
Which words do you need before learning React Server Components?
You need five words: component, bundle, bundler, hydration and serialise. Each gets a plain meaning here, and the rest of the guide builds on them.
Component. A component is a JavaScript function that returns a description of some UI, such as a heading or a button. React calls your components and builds the page from what they return.
Bundle and bundler. A bundle is the set of JavaScript files the browser downloads. A bundler is the build tool that decides what goes into it, such as webpack, Parcel or Turbopack.
Hydration. Hydration is the step where React in the browser attaches event handlers and state to HTML that already arrived from the server. Before hydration, a button is visible but does nothing when tapped.
Serialise. To serialise a value is to turn it into text so it can travel over the network, then rebuild it on the other side. A number serialises easily. A function that closes over local variables cannot be written down as text.
What are React Server Components and how does the model work?
React Server Components are components that render ahead of time in a separate environment, before your app is bundled. React's docs call that environment the server. It can be a web server handling each request, or your build machine running once.
A Server Component can be an async function. It can await a database read in the middle of rendering, because it never runs in the browser. As it renders, React writes the output as a stream of text, usually called the RSC payload.
The browser receives that payload, not the component's code. Wherever the payload says a Client Component belongs, the browser downloads that component's JavaScript and runs it. The rest is already finished UI.
One product page, from database to screen
Drag to exploreNotice that the arrow into the browser carries output, not code. Of your own components, only the Like button's code travels, next to React itself. SSR is an optional extra step.
Diagram connections:
- Product database to Server Components run (read during render)
- Server Components run to RSC payload (Flight) (serialise output)
- RSC payload (Flight) to Browser (payload, always)
- RSC payload (Flight) to Optional SSR step (first visit)
- Optional SSR step to Browser (HTML for first load)
- Client bundle to Browser (download)
- Browser to Interactive page (hydrate client parts)
- Product database: Row: name, price, costPrice
- Server Components run: Await data, render title, price, reviews
- RSC payload (Flight): Text rows: finished UI plus client slots
- Optional SSR step: Turns the payload into first-load HTML
- Browser: Shows the UI, fetches client JS
- Client bundle: LikeButton.js, its imports, plus React itself
- Interactive page: Like button hydrated, state works
An analogy helps with the hardest idea. A newspaper is printed at the press, and you receive finished pages, not the press. The crossword is the one part you work on at home with a pencil.
The press is the server, the printed pages are the RSC payload, and the crossword is a Client Component. You never need the press in your house to read the news. You do need the pencil, which is the client JavaScript.
Where the picture breaks: a newspaper is printed once, but a server can render fresh output for every request. Also, the payload is not finished HTML. It is a description of the UI that React turns into the page.
What runs on the server and what runs in the browser?
Server Components run on the server and never in the browser. Client Components run in the browser, and they can also run on the server during SSR to produce first-load HTML. The table sums up the difference.
| Question | Server Component | Client Component |
|---|---|---|
| How is it marked? | No directive. It is the default in an RSC setup. | Its file starts with 'use client', or it is imported, directly or indirectly, by a file that does. |
| Where does its code run? | On the server or at build time only. | In the browser, and during SSR if your setup uses SSR. |
| Is its code in the browser bundle? | No. Only its output is sent. | Yes, along with everything it imports. |
| Can it use useState or useEffect? | No. These hooks are not even exported in the server build of React. | Yes. That is its main reason to exist. |
| Can it be an async function that awaits data? | Yes. React waits for it during render. | No. Read a promise with the use API instead. |
| Can it read a database or a secret key directly? | Yes, because the code stays on the server. | No. Anything in its code is visible to users. |
The hooks row is easy to check. Run React 19.3.1 in Node.js with the react-server condition, which is the flag that loads React's server build. In our run, typeof React.useState printed undefined, while the normal build printed function.
What does the 'use client' directive really mark?
The 'use client' line marks a file and every file it imports as client code. React's docs say it creates a boundary in the module dependency tree, which is the map of which file imports which. It does not mark one component alone.
A directive is just a string at the very top of a file, above any imports. Quotes must be single or double, not backticks. Plain Node.js treats it as a meaningless string, so a bundler or loader has to act on it.
We checked this with the Node.js loader hook that ships in react-server-dom-webpack 19.3.1. With the hook on, requiring our LikeButton.js file returned an object tagged react.client.reference instead of the function. Without the hook, the same file returned the plain function.
Nesting inside a Client Component does not move code to the browser
Suppose the product page passes a server-rendered Reviews list into a client Tabs component as its children. Reviews stays a Server Component. React renders it on the server first, and Tabs only receives the finished result.
The rule from React's docs is short. If a Client Component's file imports a component, that usage becomes client code. If a Server Component renders it and hands it down as a prop, it stays on the server.
The boundary follows imports, not nesting
Drag to exploreNotice that Reviews sits inside Tabs on screen but is imported by the page file. Only files reachable from a 'use client' import go to the browser.
Diagram connections:
- ProductPage.js to Reviews.js (imports)
- ProductPage.js to Tabs.js (imports, passes Reviews)
- Tabs.js to formatDate.js (imports)
- Tabs.js to Reached via 'use client'?
- formatDate.js to Reached via 'use client'?
- Reviews.js to Reached via 'use client'?
- Reached via 'use client'? to Client bundle (Yes)
- Reached via 'use client'? to Stays on the server (No)
- ProductPage.js: Server: imports Tabs and Reviews
- Reviews.js: Server: no directive, reads database
- Tabs.js: Starts with 'use client'
- formatDate.js: No directive, but imported by Tabs
- Reached via 'use client'?: Checked on the import map
- Client bundle: Tabs.js and formatDate.js
- Stays on the server: ProductPage.js and Reviews.js
Two practical results follow. First, put 'use client' as low in the tree as you can, on the small interactive leaf. Second, a helper file imported by a client file is shipped to the browser, directive or not.
Which values can you pass across the server-client boundary?
You can pass only serialisable props: plain data, a few built-in types, JSX, promises and Server Functions. React's use-client reference lists the allowed types. We ran each case through react-server-dom-webpack 19.3.1 to see what happens.
| Prop value | Result | How it appeared in the payload |
|---|---|---|
| Plain object, string, number | Sent | As ordinary JSON |
| Date | Sent | A string starting $D followed by the date in standard ISO format |
| Map and Set | Sent | A separate row, referenced as $Q or $W |
| BigInt such as 10n | Sent | The string $n10 |
| Symbol.for('app.tag') | Sent | A separate row holding $Sapp.tag |
| Promise | Sent | A reference that is filled in when the promise resolves |
| Ordinary function | Error | Message says to mark it with 'use server' |
| Class instance or null-prototype object | Error | Message says only plain objects and a few built-ins work |
| Symbol('mine'), not registered | Error | Message says only Symbol.for symbols can cross |
The errors did not crash the server in our run. React passed each one to the onError callback and wrote an error row where the value should be. In production mode that row carried an empty digest, a field meant for an error ID, so the payload did not include the message.
What is the RSC payload, and what does Flight look like?
The RSC payload is the text stream a Server Component render produces. Inside React's code the format is called Flight: the server file is ReactFlightServer.js, and the package README calls it React Flight bindings. That README still labels the package experimental as of 23 October 2026. Here is a tiny render with no JSX.
const React = require('react');
const { renderToPipeableStream, registerClientReference, registerServerReference } =
require('react-server-dom-webpack/server');
const h = React.createElement;
// A stand-in for a database row. costPrice must never reach the browser.
const db = {
getProduct: async function (id) {
return { id: id, name: 'Steel water bottle', price: 349, costPrice: 180 };
}
};
// A Client Component, as a bundler would see it: only a reference.
const LikeButton = registerClientReference(
function () { throw new Error('Client code cannot run on the server'); },
'LikeButton.js',
'default'
);
// A Server Function: runs on the server, the browser only gets an ID.
async function addToCart(productId) {
return 'added ' + productId;
}
registerServerReference(addToCart, 'actions.js', 'addToCart');
// An async Server Component that reads data during render.
async function ProductPage(props) {
const product = await db.getProduct(props.id);
return h('main', null,
h('h1', null, product.name),
h('p', null, 'Price: Rs ' + product.price),
h(LikeButton, { productId: product.id, onAdd: addToCart })
);
}
const manifest = {
'LikeButton.js#default': { id: './LikeButton.js', chunks: ['like-chunk', 'like-chunk.js'], name: 'default' }
};
const stream = renderToPipeableStream(h(ProductPage, { id: 'p42' }), manifest);
stream.pipe(process.stdout);Run it with NODE_ENV=production node --conditions react-server flight.js after installing react, react-dom and react-server-dom-webpack at 19.3.1. Without the flag, the package throws an error asking for the react-server condition. Our output was three rows.
1:I["./LikeButton.js",["like-chunk","like-chunk.js"],"default"]
2:{"id":"actions.js#addToCart","bound":null}
0:["$","main",null,{"children":[["$","h1",null,{"children":"Steel water bottle"}],["$","p",null,{"children":"Price: Rs 349"}],["$","$L1",null,{"productId":"p42","onAdd":"$h2"}]]}]Read the rows from the bottom. Row 0 is the page: tag names, props and text, already rendered. Where the Like button goes, it says $L1, a pointer to row 1.
Row 1 tells the browser which module to load for the Client Component, and which chunks hold it. Chunks are the separate JavaScript files a bundler splits code into. Row 2 stands for addToCart. The browser gets only an ID, and $h2 in row 0 points to it.
Notice what is missing. The costPrice field never appears, because ProductPage did not render it or pass it on. The db object and the async ProductPage function stayed on the server.
The payload streams in pieces
The payload is not one big JSON document. When part of the tree waits inside a Suspense boundary, React sends the ready parts first. A Suspense boundary is a wrapper that shows a fallback until its children are ready.
We wrapped a Reviews component that waits 500 ms (made up) in Suspense. In one run, the first chunk arrived at about 31 ms with the title and a Loading reviews fallback. The reviews row arrived at about 531 ms as a separate row 2.
How are React Server Components different from SSR?
SSR, or server-side rendering, turns components into HTML for the first page load. React Server Components produce the RSC payload instead, and their code never ships. The two solve different problems and are often used together.
With SSR alone, every component still ships to the browser and hydrates there. The server mainly gave you HTML for a faster first paint. With Server Components, the server-only parts never hydrate at all, because the browser never gets their code.
React's docs describe SSR as optional on top of RSC. We tested the combination. A second Node.js process, started without the react-server condition, read our payload and passed it to react-dom/server.
That process produced HTML with a main element, a heading, the price paragraph and a button reading Like (0) for p42. The LikeButton function ran there during SSR, using useState. The ProductPage code never ran in that process.
| Question | SSR alone | React Server Components |
|---|---|---|
| What does the server send? | HTML for the first load | The RSC payload, optionally turned into HTML by SSR |
| Does component code ship to the browser? | Yes, all of it | Only Client Components and their imports |
| What hydrates? | The whole page | Only the Client Components |
| Can it run at build time? | Rendering HTML at build time is called static generation | Yes, React's docs list build time as an option |
For a full tour of SSR, CSR and static generation as page-level choices, read the rendering patterns guide linked at the end. Beginners can stop here: you now know the model.
How do Server Functions and the 'use server' directive work?
A Server Function is an async function that the browser can call, while its code runs on the server. You mark it with 'use server' at the top of its body, or at the top of a file to mark every export.
A common mistake is to think 'use server' marks Server Components. React's docs say plainly that there is no directive for Server Components. The 'use server' line is only for Server Functions.
Until September 2024, React called all of these Server Actions. Now a Server Action is a Server Function passed to a form's action prop or called from inside an action. Every Server Action is a Server Function, but not the other way round.
What happens when the browser calls addToCart
Drag to exploreNotice that the browser holds only an ID. The arguments it sends are whatever the user's device chooses to send.
Diagram connections:
- User taps Add to cart to Request with ID and args (call the reference)
- Request with ID and args to Decode arguments (network request)
- Decode arguments to Validate and authorise
- Validate and authorise to Run the mutation (allowed)
- Run the mutation to Serialised result
- User taps Add to cart: User → Browser
- Request with ID and args: Browser → Server: actions.js#addToCart
- Decode arguments: Server → Server: rebuild from text
- Validate and authorise: Server → Server: is this user allowed?
- Run the mutation: Server → Database: write the cart
- Serialised result: Server → Browser: return value
Arguments must be serialisable, and the list differs a little from props. FormData is allowed, so forms work. JSX, ordinary functions, class instances and browser events are not allowed as arguments.
Pass a Server Function to a form's action and React can submit the form before the JavaScript bundle has loaded. React's docs call this progressive enhancement. The useActionState hook adds pending state and the last result in a Client Component.
React's docs also say Server Functions are designed for mutations, meaning writes that change server state. They are not recommended for data fetching. Frameworks typically process one action at a time and do not cache the return value.
How do React Server Components change bundle size and data fetching?
Bundle size drops by the code of every component and library used only on the server. React's docs show a Markdown example where 75K of libraries, measured after gzip compression, leaves the bundle. That is their example, and we did not measure it ourselves.
For our product page, the Markdown library moves into the Server Component that renders the description. The browser receives the rendered paragraphs. It never downloads the library, because no client file imports it.
Data fetching changes too. Without Server Components, the page renders, then calls an API from an effect, then renders again. With them, the component awaits the database during render, so no extra round trip is needed.
Low-priority data can wait without blocking the page. React's docs show a Server Component that starts a promise and passes it to a Client Component unresolved. The Client Component reads it with the use API inside Suspense.
How do you keep secrets from leaking across the boundary?
Treat everything that crosses the boundary as public, in both directions. Data going out ends up in the payload. Arguments coming in are written by the user's device. Our tests found three easy ways to leak.
Three leaks we reproduced on React 19.3.1
- Whole objects as props. Passing a full database row sent every field, including costPrice, even though nothing displayed it. Pick fields explicitly before passing them.
- Bound arguments. We bound a Server Function to a fake internal discount code with bind. The payload carried that code in plain text, in a row the browser receives.
- Trusted arguments. A tampered call claiming a price of Rs 1 was decoded on the server exactly as sent. Look prices and permissions up on the server instead.
React's docs say to validate every Server Function argument and check that the logged-in user may perform the action. React also has taint APIs that block chosen values from reaching the client. As of 23 October 2026 they are experimental, and the stable 19.3.1 build does not export them.
Some frameworks add their own protection for values captured by inline Server Functions. That is framework behaviour, not React's, and we did not test it. Check your framework's documentation rather than assuming.
Which React 19 versions fix the Server Components security advisories?
Run a release that includes every fix, which as of 23 October 2026 means 19.3.1 or the latest patch of your 19.x line. The history explains why patching matters here.
On 3 December 2025, React disclosed CVE-2025-55182. A CVE is a public ID for a security flaw. It was rated CVSS 10.0, the top of the 0 to 10 severity scale. A flaw in how React decoded payloads sent to Server Function endpoints allowed unauthenticated remote code execution, meaning anyone could run their own code on the server. It affected react-server-dom-webpack, react-server-dom-parcel and react-server-dom-turbopack.
React's post says an app could be vulnerable even with no Server Functions, if it supports Server Components. Apps that do not use a server, or no RSC-capable framework or bundler, were not affected. Fixes landed in 19.0.1, 19.1.2 and 19.2.1. These are no longer enough, as the version callout below shows.
A follow-up post on 11 December 2025, updated on 26 January 2026, added denial-of-service issues, where a crafted request hangs or crashes the server, and a source code exposure issue. Secrets hardcoded in Server Function source could leak, but runtime values read from process.env were not affected.
How do you check that a component really stays on the server?
Search the output for something only the server should know. If a fake marker shows up in the payload or the client JavaScript, it has crossed the boundary. These steps work with any setup.
Plant a fake marker
Put a fake value such as FAKE-SUPPLIER-TERMS in a server-only field, and import a library only from a Server Component.
Build for production
Run your production build. Development builds add debug rows, such as component stacks, that production leaves out.
Search the client bundle
Search the built JavaScript files for the marker and for a distinctive string from the library. Minifiers drop names, so zero matches is a good sign, not proof. Confirm with your bundler's analyser.
Search the payload
Open the browser's network tab, load the page and search the responses for the marker. Our 19.3.1 test showed costPrice only when a whole row was passed.
Tamper with a call
Replay a Server Function request with a changed argument, using fake values only. The server should reject it or ignore the changed field.
When is using React Server Components the wrong choice for a page?
They help least when almost every part of the page is interactive, such as a drawing app or a spreadsheet. Most components there would need 'use client' anyway, so little code stays on the server.
They also need a bundler or framework that supports them, plus a server or a build step. React's docs say the underlying bundler APIs may break between minor versions, so building your own setup means pinning React. For app builders, React's docs point to full-stack frameworks that support them.
How do you explain React Server Components in a system design interview?
Start from the cost you are removing, then draw the boundary, then cover safety. Interviewers want to hear what runs where and what crosses the wire.
Name the problem
Static parts of the page ship code and wait for data in the browser. Server Components move that work next to the data.
Draw the split
Mark the server tree, the 'use client' leaves and the RSC payload between them. Say that the boundary follows imports, not nesting.
Show the wire
Explain that only serialisable values cross, that Client Components arrive as module references, and that the payload streams with Suspense.
Place SSR correctly
Say SSR is optional and only turns the first load into HTML. Server Components decide which code ships at all.
Cover writes and safety
Use Server Functions for mutations. Validate and authorise every argument, pass only needed fields, and keep the react-server-dom package patched.
Questions people ask
- Are React Server Components the same as server-side rendering?
- No. SSR turns components into HTML for the first page load, and all component code still ships to the browser. React Server Components produce an RSC payload, and their code never ships. A setup can use both: React's docs describe SSR as an optional step that turns the payload into HTML.
- Does 'use server' make a component a Server Component?
- No. React's docs say there is no directive for Server Components. In an RSC setup, components are Server Components by default. The 'use server' directive marks Server Functions, which are async functions the browser can call over the network. Use 'use client' to mark where client code begins.
- Can a Client Component render a Server Component?
- Yes, if the Server Component is passed in as a prop such as children. A Server Component file higher up imports both, renders the Server Component first and hands the result down. If a Client Component imports that file instead, the imported component becomes client code, so pass it in as children.
- Can I use useState or useEffect in a Server Component?
- No. Server Components never run in the browser, so state and effects have no meaning there. In our test on React 19.3.1, the server build of React did not even export useState or useEffect. Move the interactive part into a small Client Component and pass it data as props.
- Do I need a framework to use React Server Components?
- You need a bundler or framework that supports them, because something must read the directives and split the code. React publishes react-server-dom-webpack and similar packages for this. React's docs say their lower-level APIs may break between minor versions, so the usual path is a framework that supports them.
- Are React Server Components safe to use after the security advisories?
- They can be, if you stay patched and write Server Functions carefully. Advisories continued through 2026, the latest on 14 October. As of 23 October 2026, React's GitHub security page lists fixes up to 19.0.9, 19.1.10, 19.2.9 and 19.3.1. Validate every Server Function argument, check the user's permission and never pass whole database rows to Client Components.
Sources and further reading
- React docs: Server Components reference
- React docs: 'use client' directive reference
- React docs: 'use server' directive reference
- React docs: Server Functions reference
- React blog: Critical Security Vulnerability in React Server Components (3 December 2025)
- React blog: Denial of Service and Source Code Exposure in React Server Components (11 December 2025)
- React GitHub security advisories list
- React GitHub advisory GHSA-xqc2-xf4c-x3h3 (14 October 2026)
- React source at v19.3.1: ReactFlightServer.js