---
title: "React Server Components explained: what reaches the browser"
description: "React Server Components in Next.js 16, in a live demo: what stays on the server, what ships as JavaScript, and what the HTML and RSC payload carry."
canonical_url: "https://www.outofplace.space/blog/react-server-components-explained"
language: "en"
last_updated: "2026-10-23"
alternates:
  pl: "https://www.outofplace.space/pl/blog/react-server-components-co-trafia-do-przegladarki.md"
---

# React Server Components explained: what reaches the browser

> React Server Components in Next.js 16, in a live demo: what stays on the server, what ships as JavaScript, and what the HTML and RSC payload carry.

Published 2026-10-23 · Category: Next.js & React · Tags: React, Next.js

React Server Components are components whose code runs only on the server. In the Next.js App Router, every page and layout is one by default, and the browser gets what they render, as HTML and again in the RSC payload, never their code. Only a file that starts with `"use client"`, plus everything it imports, ships as JavaScript. Those Client Components are still rendered to HTML on the server first, then hydrated in the browser. Data crosses from server to client only through serializable props, and a Server Component passed in as `children` stays server output even inside a Client Component. We built one demo that shows all of this on Next.js 16.4.0 with React 19.3.0, and nine Playwright tests check the behaviour this post shows in it. In your own app, run patched versions: the [React and Next.js RCE fixes of October 2026](https://www.outofplace.space/blog/dev-digest-2026-10-18) cover the same layer.

Live demo: https://lab.outofplace.space/demos/react-server-components-explained (Next.js 16.4.0; source at commit b5049c6: https://github.com/outof-place/outofplace-lab/tree/b5049c6980621147d3446a5748a1c6f1d74d4cbc/app/demos/react-server-components-explained)

## What are React Server Components?

React's own reference describes [Server Components](https://react.dev/reference/rsc/server-components) as components that render ahead of time, "in an environment separate from your client app or SSR server". In Next.js that environment is the server, at build time or per request. You never mark a component as a Server Component: [there is no directive for Server Components](https://react.dev/reference/rsc/server-components#there-is-no-directive-for-server-components), and `"use server"` marks Server Functions. What you mark is the other side, with `"use client"`.

The Next.js guide to [the server and client boundary](https://nextjs.org/docs/app/guides/server-and-client-boundary#rendering-environments) puts the difference in one table. Here it is with what that means when you write the code:

| | Server Component | Client Component |
| --- | --- | --- |
| Marked by | nothing (the default in `app/`) | `"use client"` at the top of the file |
| Renders on the server | yes | yes, to HTML |
| Runs in the browser | no | yes, it hydrates there |
| Its code and imports ship as JavaScript | no | yes |
| `useState`, `useEffect`, `onClick` | no | yes |
| Can read secrets, files, a database | yes | no: treat it as browser code |

## What reaches the browser: HTML, the RSC payload and client JavaScript

On a [first load in Next.js](https://nextjs.org/docs/app/getting-started/server-and-client-components#on-the-client-first-load), the docs name three things the browser uses: HTML for a fast, non-interactive first paint, the RSC payload to reconcile the server and client trees, and JavaScript to hydrate the Client Components. The RSC payload is a compact, serialized form of the rendered Server Component tree. It holds their rendered output, placeholders for the Client Components with references to their JavaScript files, and every prop passed from a Server Component to a Client Component.

The demo's Server Component signs the request time with an HMAC key. The key and Node's `crypto` module sit in a separate file that starts with `import "server-only"`:

app/demos/react-server-components-explained/signing.ts (https://github.com/outof-place/outofplace-lab/blob/b5049c6980621147d3446a5748a1c6f1d74d4cbc/app/demos/react-server-components-explained/signing.ts)

```ts
import "server-only";

import { createHmac } from "node:crypto";

// A made-up key, public in this repo. In an app it comes from process.env. Either way it lives
// only in the server module graph: import "server-only" turns an import from a "use client"
// file into a build error.
const SIGNING_KEY = "lab-demo-key-stays-on-the-server";

/** The first 12 hex characters of an HMAC-SHA256 of `value`, made with Node's crypto module. */
export function sign(value: string): string {
  return createHmac("sha256", SIGNING_KEY).update(value).digest("hex").slice(0, 12);
}
```

The card that uses it is an async function with no directive:

app/demos/react-server-components-explained/server-card.tsx (https://github.com/outof-place/outofplace-lab/blob/b5049c6980621147d3446a5748a1c6f1d74d4cbc/app/demos/react-server-components-explained/server-card.tsx)

```tsx
import { io } from "next/cache";

import { sign } from "./signing";
import styles from "./rsc.module.css";

// Server Components: no directive, so they stay in the server module graph. The browser
// receives what they render (as HTML and in the RSC payload), never this code.

const renderedOn = () => (typeof window === "undefined" ? "server" : "browser");

export async function ServerCard() {
  // With Cache Components on, io() keeps what follows out of the build's static shell,
  // so the server signs a fresh time on every request.
  await io();
  const signedAt = new Date().toISOString();

  return (
    <section className={styles.card} aria-labelledby="server-component">
      <h2 id="server-component">Server Component</h2>
      <dl className={styles.rows}>
        <div>
          <dt>Rendered on</dt>
          <dd>{renderedOn()}</dd>
        </div>
        <div>
          <dt>Signed at</dt>
          <dd>
            <time dateTime={signedAt}>{signedAt.slice(11, 19)} UTC</time>
          </dd>
        </div>
        <div>
          <dt>Signature</dt>
          <dd>
            <code>{sign(signedAt)}</code>
          </dd>
        </div>
      </dl>
    </section>
  );
}

// Passed to the Client Component as children: it renders here, on the server, and the
// Client Component only places its output.
export function ServerNote() {
  return <span>{renderedOn()}, passed in as children</span>;
}
```

The tests check what actually arrives, on a production build:

- The HTML response holds the signature in its markup, and the same signature again inside an inline `<script>` in that response: that is the RSC payload, since [the RSC payload ships with the HTML](https://nextjs.org/docs/app/guides/server-and-client-boundary#state-and-interactivity) on the first load.
- The signature is an HMAC-SHA256, with the key from `signing.ts`, of the full ISO timestamp, date and milliseconds included, which sits in the `datetime` attribute of the `<time>` element; the page shows only its hours, minutes and seconds. The test reads that attribute and computes the HMAC again itself.
- While the page loads, hydrates and takes three clicks, the key is in none of the responses: not the HTML, not the payload, not a JavaScript file.
- A string from `counter.tsx` is in a JavaScript file. The strings that exist only in `server-card.tsx` and `page.tsx` are in none.

You can check the same on your own app. Open the page source and search for a value a Server Component rendered: it appears in the markup and again inside a script. Then search the loaded JavaScript files in DevTools for a string that only a Server Component's source contains: it should not be there.

## How to tell which code stays on the server

One rule decides it: [code crosses the boundary through imports](https://nextjs.org/docs/app/guides/server-and-client-boundary#crossing-the-boundary). A file with `"use client"` is an entry into the client module graph, and every module it imports is pulled into the client bundle, with or without its own directive. A file no client file imports stays on the server.

The demo's Client Component is a counter:

app/demos/react-server-components-explained/counter.tsx (https://github.com/outof-place/outofplace-lab/blob/b5049c6980621147d3446a5748a1c6f1d74d4cbc/app/demos/react-server-components-explained/counter.tsx)

```tsx
"use client";

import { useState, useSyncExternalStore, type ReactNode } from "react";

import styles from "./rsc.module.css";

const subscribe = () => () => {};

// A Client Component: "use client" puts this file and everything it imports in the browser's
// JavaScript. It still renders to HTML on the server first, then hydrates in the browser.
export function Counter({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);
  // The server snapshot (false) is used on the server and during hydration; once hydrated,
  // React renders again with the browser's snapshot (true).
  const hydrated = useSyncExternalStore(subscribe, () => true, () => false);

  return (
    <section className={styles.card} aria-labelledby="client-component">
      <h2 id="client-component">Client Component</h2>
      <dl className={styles.rows}>
        <div>
          <dt>Rendered on</dt>
          <dd>{hydrated ? "browser, after hydration" : "server, not hydrated yet"}</dd>
        </div>
        <div>
          <dt>Count</dt>
          <dd>
            <output aria-label="Count">{count}</output>
            <button type="button" onClick={() => setCount((n) => n + 1)}>
              Add one
            </button>
          </dd>
        </div>
        <div>
          <dt>Children</dt>
          <dd>{children}</dd>
        </div>
      </dl>
    </section>
  );
}
```

The "Rendered on" row shows that a Client Component runs in both places. With JavaScript off, the browser shows only the server's HTML, and the row reads "server, not hydrated yet" with the count at 0. With JavaScript on, it reads "browser, after hydration". It switches through [`useSyncExternalStore`](https://react.dev/reference/react/useSyncExternalStore#adding-support-for-server-rendering), whose `getServerSnapshot` is used only on the server and during hydration. [React's hydrateRoot docs](https://react.dev/reference/react-dom/client/hydrateRoot#hydrating-server-rendered-html) list a `typeof window` check in render as a common cause of hydration errors, because the server and the browser would render different output.

The way to make the server side hold is [`import "server-only"`](https://nextjs.org/docs/app/getting-started/server-and-client-components#preventing-environment-poisoning) in every module that must never reach the browser. Importing such a module into a Client Component is then a build-time error, not a quiet leak. In Next.js you don't need to install the package: Next.js handles the import itself. Environment variables follow a similar rule: only the ones prefixed with `NEXT_PUBLIC_` are included in the client bundle, and Next.js replaces the rest with an empty string.

## Server Components inside Client Components: the children pattern

The page puts a second Server Component, `ServerNote`, inside the counter, as `children`:

app/demos/react-server-components-explained/page.tsx (https://github.com/outof-place/outofplace-lab/blob/b5049c6980621147d3446a5748a1c6f1d74d4cbc/app/demos/react-server-components-explained/page.tsx)

```tsx
import { Suspense } from "react";

import { Counter } from "./counter";
import { ServerCard, ServerNote } from "./server-card";
import styles from "./rsc.module.css";

// The page is a Server Component too: pages and layouts are, unless a file says "use client".
export default function Page() {
  return (
    <div className={styles.stack}>
      <Suspense fallback={<p className={styles.pending}>Signing on the server…</p>}>
        <ServerCard />
      </Suspense>

      <Counter>
        <ServerNote />
      </Counter>

      <p className={styles.hint}>
        Add one changes the count in the browser and leaves the signature as it is. <a href="">Reload</a> and the
        server signs a new time.
      </p>
    </div>
  );
}
```

On a hydrated page, the note still reads "server, passed in as children", and three clicks re-render the counter without changing it. The docs describe why: a rendered React element is serializable, so the Server Component's output crosses the boundary as data, and the Client Component only places it. [Server Components passed as children](https://nextjs.org/docs/app/getting-started/server-and-client-components#interleaving-server-and-client-components) are rendered on the server ahead of time, and the RSC payload carries their result. `Page` owns `ServerNote` (its source contains the JSX), so `ServerNote` renders on the server; `Counter` is only its parent in the rendered tree. By the docs' rule, importing `ServerNote` into `counter.tsx` instead would pull `server-card.tsx` into the client module graph, with `signing.ts` behind it, and `import "server-only"` would turn that into a build error.

The same pattern is how you use React context: [context is not supported in Server Components](https://nextjs.org/docs/app/getting-started/server-and-client-components#context-providers), so a provider is a Client Component that takes `children`, and the server-rendered tree goes inside it.

## Is RSC the same as SSR?

No, and the demo shows both at once. The counter's HTML comes from the server: that is server-side rendering, and [server-side rendering predates RSC](https://nextjs.org/docs/app/guides/server-and-client-boundary), as the boundary guide notes; Next.js does it at build time or per request. The counter is still a Client Component, because its code ships and runs again in the browser. "Server-rendered" says how the HTML was made. "Server Component" says where the component's code runs and whether it ships.

When the server renders is a separate question. The signed time in the demo is per request: the reload test sees a new one after a reload. The lab runs with [Cache Components](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents) on, and the [`await io()` reference](https://nextjs.org/docs/app/api-reference/functions/io) explains what that means for a value like `new Date()`: you capture it once [with use cache, into the static shell](https://www.outofplace.space/blog/nextjs-use-cache#how-long-a-use-cache-entry-lives), or keep it out of the static shell with `await io()`, which the card does inside `<Suspense>`.

## Why a click doesn't reach the server, and a reload does

Three clicks on Add one set the count to 3 with no network request at all, and the time, the signature and the note stay as they were. The boundary guide says a Server Component can render again when Next.js renders the route again, for example on a navigation, a refresh or after revalidation. In the demo, a state change in the browser re-renders the Client Component without any request to the server and leaves the Server Components' output as it was.

Reload and the server runs again: the test sees a later time, a new signature and the count back at 0, because a full load starts the Client Component from its initial state.

## How to split a Next.js page into Server and Client Components

**1. Start with Server Components.**
Pages and layouts in `app/` are Server Components by default. Fetch data and read secrets there, with no directive.

**2. Put use client on the smallest interactive part.**
Add `"use client"` to the file that needs state, effects, event handlers or browser APIs, such as a button, a filter or a form field, not to the page around it. Everything that file imports ships too.

**3. Mark server modules with server-only.**
Add `import "server-only"` to every module that reads a secret, a database or the file system. A wrong import then fails the build.

**4. Pass small, serializable props.**
Send a Client Component only the fields it shows: strings, numbers, plain objects, arrays, Dates, Maps, Sets, Promises and JSX. Never pass a whole database record, and never pass a plain function: only Server Functions cross the boundary.

**5. Pass server UI as children.**
When server-rendered UI must sit inside a Client Component, such as a modal, a tab panel or a context provider, pass it as `children` or another prop from a Server Component instead of importing it.

**6. Check a production build.**
Run `next build` and `next start`, open the page source and search for a server-only string in the loaded JavaScript, as the demo's test does.

## What breaks the server and client split

### Props are sent to the browser

Every prop a Server Component passes to a Client Component is serialized into the RSC payload. The [Next.js data security guide](https://nextjs.org/docs/app/guides/data-security#passing-data-from-server-to-client) shows the usual leak: a page passes a full user row to a `"use client"` profile component, and every column of that row reaches the browser, even the ones the component never renders. Return a small object with only the public fields from a server-only data layer. React's taint APIs can block a specific object or value from reaching the client (in Next.js behind [`experimental.taint`](https://nextjs.org/docs/app/guides/data-security#tainting)), but the guide treats them as an extra layer, not a replacement for filtering.

### Functions don't cross

A plain function can't be a prop from a Server Component to a Client Component: React's [list of serializable props](https://react.dev/reference/rsc/use-client#serializable-types) allows only Server Functions, and leaves out class instances and objects with a null prototype too. So an `onClick` defined on the server can't work. Define handlers inside the Client Component, or pass a Server Function marked with `"use server"`, which crosses as a reference.

### "use client" too high up

`"use client"` on a layout or a page moves everything below it into the client graph: each import and each component that file renders directly. The [Next.js docs](https://nextjs.org/docs/app/getting-started/server-and-client-components#reducing-js-bundle-size) recommend putting it on the interactive leaf, such as a search box, and keeping the layout around it a Server Component.

### Third-party components without the directive

A library component that uses `useState` but has no `"use client"` errors when a Server Component renders it. The docs' fix for [third-party components](https://nextjs.org/docs/app/getting-started/server-and-client-components#third-party-components) is a file of your own that has the directive and re-exports it.

## React Server Components vulnerabilities: keep React and Next.js patched

React's 19.3.1 release lists one change under React Server Components, ["Improve transport protocol"](https://github.com/react/react/releases/tag/v19.3.1). On 14 October 2026, React published the critical advisory [Unauthenticated Remote Code Execution in React Server Components](https://github.com/react/react/security/advisories/GHSA-xqc2-xf4c-x3h3). It names the packages `react-server-dom-webpack`, `react-server-dom-turbopack` and `react-server-dom-parcel`, from 19.0.0 to 19.3.0, patched in 19.0.9, 19.1.10, 19.2.9 and 19.3.1; the `react` package itself is not on its list, so check those packages in your lockfile too. The same day, [Next.js 16.4.1](https://github.com/vercel/next.js/releases/tag/v16.4.1) fixed three advisories of its own. The dev digest of 18 October 2026, linked at the top and under Further reading, lists what each one covers and which version to take.

The demo was verified on Next.js 16.4.0, inside the range of the Next.js advisories, with React 19.3.0. We have not re-run it on the patched versions yet; build your own app on them.

## Further reading

- [Next.js: Server and Client Components](https://nextjs.org/docs/app/getting-started/server-and-client-components): When to use each, the RSC payload, interleaving with children and server-only.

- [Next.js: The Server and Client Boundary](https://nextjs.org/docs/app/guides/server-and-client-boundary): Where each component runs, how code and data cross, and owner versus parent.

- [Next.js: Data security](https://nextjs.org/docs/app/guides/data-security): What props send to the browser, data access layers, tainting and Server Action checks.

- [React: Server Components](https://react.dev/reference/rsc/server-components): The reference: what Server Components are, async components and stability in React 19.

- [React: 'use client'](https://react.dev/reference/rsc/use-client): How the directive marks a module and its imports, and which props are serializable.

- [React: Unauthenticated Remote Code Execution in React Server Components](https://github.com/react/react/security/advisories/GHSA-xqc2-xf4c-x3h3): The advisory of 14 October 2026: affected packages and the patched 19.x versions.

- [Dev digest: React and Next.js RCE fixes, Opus 5.5 fast mode](https://www.outofplace.space/blog/dev-digest-2026-10-18): Next.js news for 11 to 18 Oct 2026: patch React and Next.js for three critical RCEs, fast mode for Claude Opus 5.5 leaves preview and Python on Vercel goes GA.
