Server Components

Published:

We have used components to describe the page, state to change it, and Effects to synchronize with the outside world. In those examples, the component code runs in the browser.

Consider a page that displays a Markdown document and a button that hides or shows it. The button needs browser state. Reading the document and turning Markdown into content are different jobs: they can happen before the page reaches the browser.

That leaves the next hole:

if only part of a page needs browser interaction, how can the rest be rendered without sending its component code and dependencies to the browser?

The file the browser cannot read #

Start with the two requirements in one component. This is a failed attempt:

'use client';

import { readFile } from 'node:fs/promises';
import { useState } from 'react';

function Document() {
  const [open, setOpen] = useState(true);
  // Read the Markdown file and render it here?
  return <button onClick={() => setOpen(!open)}>{open ? 'Hide' : 'Show'}</button>;
}

The state and click handler belong in the browser, but a browser cannot use Node's filesystem API to read a file from the machine that built the site. Marking the module as client code does not move that file into the browser. We could serve the document through an HTTP endpoint and fetch it, but this page's content is already available at build time.

The failure reveals two environments and two jobs:

  • a build environment can read the local document and prepare its content
  • the browser can keep track of the button's state and respond to clicks

We need a way to compose their results into one React tree.

A component that runs before the browser #

A Server Component is rendered in an environment separate from the client application. That environment can be a web server handling a request, but it can also be the build machine. React allows Server Components to be async, so they can await data while rendering.[1]

For our example, the server environment exists during the build. It reads a local Markdown file and prepares the document before any visitor opens the page. No long-running application server is needed to serve the exported result.

The companion's Page reads a repository-owned file, parses it as Markdown, and sanitizes the resulting HTML. Here is the relevant part of app/page.jsx; the surrounding heading and resource links are omitted:

import 'server-only';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { marked } from 'marked';
import sanitizeHtml from 'sanitize-html';
import Expandable from './Expandable';

export default async function Page() {
  const markdown = await readFile(
    join(process.cwd(), 'content/preview.md'),
    'utf8',
  );
  const html = sanitizeHtml(marked.parse(markdown));

  return (
    <Expandable title="Article preview">
      <article className="preview" dangerouslySetInnerHTML={ { __html: html } } />
    </Expandable>
  );
}

The server-only import helps the framework detect an accidental client import. dangerouslySetInnerHTML tells React to insert HTML; it needs care because raw HTML can execute unwanted content. The example sanitizes the parser output, even though its input is a file in the repository.

The Server Component's implementation and its server-only dependencies are staying on the server side. The rendered result is sent to the browser.

Never render private data or pass it to a Client Component unless the visitor is allowed to receive it.

How does the result remain interactive?

The interactive boundary #

A Server Component cannot own interactive useState, browser event handlers, or an Effect that runs in the visitor's browser. A Server Component can render a Client Component for that work. Put 'use client' at the start of a module to mark that module as a client entry point. A compatible framework and bundler use it to establish the boundary when server code imports the module.[2]

The companion's app/Expandable.jsx owns the state and the click handler. The Server Component supplies the content:

'use client';

import { useId, useState } from 'react';

export default function Expandable({ title, children }) {
  const [expanded, setExpanded] = useState(true);
  const contentId = useId();

  return (
    <section className="expandable">
      <header>
        <h2>{title}</h2>
        <button
          type="button"
          aria-expanded={expanded}
          aria-controls={contentId}
          onClick={() => setExpanded((current) => !current)}
        >
          {expanded ? 'Hide details' : 'Show details'}
        </button>
      </header>
      <div id={contentId} hidden={!expanded}>{children}</div>
    </section>
  );
}

The toggle starts open. aria-expanded reports its state, and aria-controls identifies the controlled content. useId supplies the identifier. React transfers supported values across the boundary; it does not send an ordinary closure with access to server memory.

Try the composed result below. Hide details and Show details change the Client Component's state. The Markdown content has already been prepared by the Server Component during the build.

Open the demo on its own. You can also inspect the Server Component, Client Component, and Markdown input.

The boundary constrains props. Strings, numbers, arrays, plain objects containing supported values, and React elements are examples of values React can transfer. React's supported set is broader than JSON, and not every JavaScript value is supported. An ordinary function cannot be passed as a prop from a Server Component to a Client Component; Server Functions have a separate protocol.[2]

The toggle handler therefore belongs inside the Client Component. Its content can arrive as a children prop.

A client wrapper around server content #

JSX children are props, as we saw in Props. In an RSC application, a Server Component can construct a child and pass the resulting UI to a Client Component:

// Server-side composition, shown schematically
<Expandable>
  <DocumentContent />
</Expandable>

Expandable owns the interactive state. DocumentContent is supplied by its server parent. The wrapper can show or hide that supplied child without importing and executing the server component itself.

This is easier to understand when we separate two trees. 'use client' marks a boundary in the module dependency tree: the marked module and its transitive imports are client code. In the rendered tree, a server parent can pass server-rendered content into a client wrapper as children.[2]

If the client module instead imports and calls DocumentContent itself, that import becomes part of its client dependency graph. It cannot thereby gain access to node:fs on the build machine. Pass the rendered content from the server parent instead.

Server Components, HTML, and hydration #

There are two related questions:

  1. Where do component implementations run, and which code must reach the browser?
  2. How does the browser receive an initial page and make it interactive?

Server Components address the first. Their rendered result and references to Client Components are represented in an RSC payload. A framework can also use that result to produce initial HTML. In this static export, both operations happen during the build.[4]

The HTML gives the browser initial content. React then hydrates the client-rendered parts: it attaches React's interactive behavior to the existing HTML using the corresponding client code. The toggle works after that code loads. The framework handles this setup; the earlier browser examples used createRoot because they started with an empty mount node.[3]

Server-side HTML rendering produces the initial markup; Server Components determine where component work runs and which implementation code reaches the browser. A Client Component can participate in generating the initial HTML on the server and later run in the browser for hydration and updates. 'use client' means its code belongs to the client module graph; it does not promise that the component is evaluated only in the browser.

The boundary is not a server API #

In this example, changing the local Markdown file requires another build and deployment. A click changes local browser state; it does not reread the file or execute the Server Component again. A request-time RSC application can render Server Components on a server and deliver an updated result, but that requires a running server and an integration that handles the request.[1]

There is no 'use server' directive for declaring a Server Component. 'use server' marks Server Functions, which can be called from client code through a separate request mechanism.

React Server Components in React 19 are stable, while the lower-level APIs used by frameworks and bundlers to implement support can change between React minor versions. Use a compatible framework integration rather than assuming a browser createRoot playground can execute server modules.[1]

Filling the hole #

The Markdown page needed two environments. The build environment could read the file and prepare content; the browser needed only the code that makes the control interactive. React composes those parts across the Server and Client Component boundary.

The important distinction is between code and output. The browser receives the rendered content and client code, while the Server Component implementation and its server-only dependencies stay in the build environment. The content itself is still public to anyone who receives the page.

Final definition #

A React Server Component is a component rendered in an environment separate from the client application, either at build time or on a server. Its output can be composed with Client Components, whose code is available to the browser for interaction.

The RSC result can be used to produce initial HTML. Hydration attaches client behavior to that HTML; neither HTML rendering nor hydration alone is the Server Component protocol.

Summary #

  • Server Components can read build-time or server-side data without sending their implementation to the browser.
  • 'use client' marks a client module boundary; it does not mean the initial HTML was produced only in the browser.
  • Props crossing the boundary must use React-supported values. A server parent can pass rendered JSX as children to a client wrapper.
  • Interactive state and handlers belong in Client Components; ordinary server closures cannot be passed to them.
  • Initial HTML, the RSC result, and client JavaScript play different roles; hydration connects client behavior to existing HTML.
  • Static exports need a rebuild for changed build-time content. Request-time rendering and Server Functions need server support.

References

  1. React: Server Components (opens in a new tab) · Back
  2. React: `use client` (opens in a new tab) · Back
  3. React: `hydrateRoot` (opens in a new tab) · Back
  4. Next.js: Static exports (opens in a new tab) · Back
  5. React: `use server` (opens in a new tab)