cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

Server Components

Learn what React Server Components are: async components that run on the server, 'use client', props that cross the boundary, and the RSC payload.

By the end, you can
  • 01
    Tell Server Components from Client ComponentsSay where each runs, what each can do, and how 'use client' marks the boundary between them in your files.
  • 02
    Pass data and JSX across the boundaryKnow which props can travel from a Server Component to a Client Component, and pass server-rendered JSX into a Client Component as children.
  • 03
    Read the RSC payloadFollow a real payload row by row, and explain how it differs from the HTML that server rendering sends.

A product page whose code stays on the server

Every component so far ran in the browser. Even with server rendering, the browser downloads every component's code and runs it again to hydrate the page. For a product page, that's a lot of code for parts that never change once they're on screen: the name, the description, the reviews. And the data comes later still. The component has to ask an API for it in an Effect, after the page has loaded:

The product page as a Client Component that fetches in an EffectJSX
"use client";
import { useEffect, useState } from "react";
import AddToCart from "./AddToCart.jsx";

export default function ProductPage({ id }) {
  const [product, setProduct] = useState(null);
  useEffect(() => {
    fetch(`/api/products/${id}`)
      .then((response) => response.json())
      .then(setProduct);
  }, [id]);

  if (!product) return <p>Loading...</p>;
  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <AddToCart productId={product.id} price={product.price} />
    </main>
  );
}

Lines 7 to 11 fetch the product only after the first render, so line 13 shows Loading... first. You also need an API route on the server for /api/products/green-tea. (This block has no result: there's no API here.)

Server Components turn this around. A Server Component runs on the server, before anything reaches the browser, so it can read the database itself. Only what it rendered is sent; its code never is. The parts that need clicks stay ordinary components, now called Client Components. Here is the same page. The result is live: wait a moment for the server, then click Add to cart.

A product page made of a Server Component and a Client ComponentPop out in the code editor (opens in a new tab)JSX
// main.jsx: asks the pretend server for ProductPage, as a framework does for a page.import { showServerPage } from "./framework.jsx";import ProductPage from "./ProductPage.jsx";import AddToCart from "./AddToCart.jsx"; // A bundler finds the files that start with "use client" and lists them like this.const clientFiles = { "./AddToCart.jsx": AddToCart }; showServerPage(<ProductPage id="green-tea" />, clientFiles);
ResultLoading…

Open the ProductPage.jsx tab. Line 4 is an async function, and line 5 waits for the product from the database with await, right inside the component: no Effect, no state, no API. Lines 8 and 9 render the name and the description. Line 10 renders AddToCart, with the product's id and price as props.

Now open AddToCart.jsx. Line 1, "use client", marks it as a Client Component: it has state on line 5 and an onClick on line 7, so it must run in the browser. db.js is a pretend database that waits 0.4 seconds, as a real query might.

What runs where in these examples

A live example can't run a real server, so each one runs a small model of one, written for this lesson. In main.jsx, line 9 asks it for ProductPage, as a framework does when a browser asks for a page. The model calls the Server Components and writes the result as rows of text, the RSC payload; then its browser half turns the rows back into elements, and the real React 19.3.1 shows them. The result shows the rows too. For every example in this lesson, the model writes exactly the rows that React's own server package, react-server-dom-webpack 19.3.1, wrote for the same code; the lesson's test checks each one. Line 7 lists the Client Components, as a bundler would after finding the files that start with "use client".

Under the page, the result lists two rows and one file: Client Component code the browser loads: ./AddToCart.jsx. ProductPage.jsx and db.js aren't there. They ran on the server and stayed there.

Left, a server window holding ProductPage.jsx and db.js, labelled code stays here. An arrow labelled the rendered page and where AddToCart lives leads right, to a browser window showing Green tea, Loose leaf, 100 g and an Add to cart button, with add-to-cart.js under it as the only code downloaded.serverProductPage.jsxdb.jscode stays hererendered page,where AddToCart isbrowserGreen teaLoose leaf, 100 gAdd to cartadd-to-cart.js
ProductPage and db.js run on the server and stay there. The browser gets what ProductPage rendered, plus the address of AddToCart's code, the only component code it downloads.
Real-life analogyA restaurant kitchen

In a restaurant, the meal is cooked in the kitchen, where the ovens and the pantry are, and only the plate comes out. You don't need a stove at your table. What you do need at the table, you get there: salt, pepper, a knife.

In real life: The kitchen cooks with its ovens and knives
In React: A Server Component renders with the database and server libraries
In real life: The plate arrives ready to eat
In React: The browser gets the rendered result
In real life: The ovens never come to your table
In React: The Server Component's code never reaches the browser
In real life: The salt and pepper sit on your table, for you to use
In React: Client Components run in the browser, for clicks and typing
The main idea

Server Components are components that run only on the server, or once when the app is built. They can be async and read data directly, and their code is never sent to the browser: only what they render is. Client Components, marked with "use client", are the ones that run in the browser, with state and events. A page mixes both.

Server Components and Client Components

In an app that supports Server Components, like one built with Next.js's App Router, every component is a Server Component unless you say otherwise. They need a framework, because something has to run them on a server and decide which files go to the browser; the React frameworks lesson compares them.

Being on the server is what gives Server Components their powers. This one reads a Markdown file from the server's disk and turns it into HTML with a Markdown library:

ProductDetails.jsx: server-only code in a Server ComponentJSX
// ProductDetails.jsx: a Server Component. No "use client", so it runs on the server only.
import { readFile } from "node:fs/promises";
import { marked } from "marked";

export default async function ProductDetails({ id }) {
  const text = await readFile(`./descriptions/${id}.md`, "utf8");
  return <article dangerouslySetInnerHTML={{ __html: marked(text) }} />;
}

Line 2 imports readFile from Node.js, which only exists on a server. Line 3 imports a Markdown library. Line 6 reads the file while rendering. The browser receives the finished <article>; it never downloads marked, or anything else this file imports. (This block runs on a server, so it has no result.)

The other side of the same coin: a Server Component renders once and is gone. It can't keep state, run Effects, or handle a click, because none of that happens on the server. That's what "use client" is for:

AddToCart.jsx: the directive marks a file, and its importsJSX
// AddToCart.jsx
"use client";
import { useState } from "react";
import { formatPrice } from "./formatPrice.js";

export default function AddToCart({ productId, price }) {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Add to cart: {formatPrice(price)}</button>;
}

"use client" on line 2 must come before any import (a comment above it is fine). It marks a boundary in the tree of imports, not just one component: AddToCart.jsx is client code, and so is everything it imports, like formatPrice.js on line 4, even though that file has no directive. All of it is sent to the browser. A file that only Server Components import stays on the server.

A tree of files. ProductPage.jsx at the top imports db.js and AddToCart.jsx. AddToCart.jsx starts with use client and imports formatPrice.js. A window titled sent to the browser holds AddToCart.jsx and formatPrice.js; db.js and ProductPage.jsx stay outside it, on the server.ProductPage.jsxdb.jsserver onlysent to the browser"use client"AddToCart.jsxformatPrice.js
"use client" in AddToCart.jsx marks a boundary in the tree of imports. AddToCart.jsx and everything it imports, like formatPrice.js, is client code and is sent to the browser. ProductPage.jsx and db.js never are.

How to read the table: each row asks one question of both kinds of component.

Server Components and Client Components
QuestionServer ComponentClient Component
Where does it run?On the server, for each request, or once when the app is built.In the browser. Frameworks also run it on the server to make the first HTML.
Is its code sent to the browser?No. Only what it rendered is sent.Yes, with everything it imports.
Can it use state, Effects and onClick?No. It renders once and is gone.Yes. That's what it's for.
Can it read the database or files directly?Yes, with await, because it runs where they are.No. It asks the server, like any browser code.
Can it be an async function?Yes.No. It reads promises with use instead.
How do you mark it?You don't: in an app that supports them, components are Server Components unless marked.With "use client" at the top of its file, or by being imported from such a file.

"Client Component" doesn't mean "only in the browser". Frameworks still render Client Components on the server to make the first HTML, then hydrate them, as the hydration lesson showed. The difference is that a Server Component never runs in the browser.

Async components read data while they render

The ProductPage above was an async function, and React waited for its await before using what it returned. Only Server Components can do that. A Client Component that needs data from a promise reads it with use, inside a Suspense boundary.

The two work together. A Server Component can start a slow request without waiting for it, and hand the promise to a Client Component, which shows the rest of the page first:

Passing a promise of reviews to a Client ComponentPop out in the code editor (opens in a new tab)JSX
// main.jsx: asks the pretend server for ProductPage, as a framework does for a page.import { showServerPage } from "./framework.jsx";import ProductPage from "./ProductPage.jsx";import Reviews from "./Reviews.jsx"; // A bundler finds the files that start with "use client" and lists them like this.const clientFiles = { "./Reviews.jsx": Reviews }; showServerPage(<ProductPage id="green-tea" />, clientFiles);
ResultLoading…

In ProductPage.jsx, line 5 waits for the product: the page can't show without it. Line 6 starts loading the reviews but doesn't await: reviewsPromise is a promise. Line 10 passes it to Reviews, a Client Component. In Reviews.jsx, line 5 reads it with use, and lines 17 to 19 wrap that in a Suspense boundary.

Watch the result: the heading comes after 0.4 seconds with Loading reviews... under it, and the reviews about a second and a half later. In the payload, row 0 holds "reviewsPromise":"$@2": a promise, whose value will arrive as row 2. Row 2 comes last, when the database answers. Press Run again to watch it once more.

Server Components run on the server for each request, or once at build time for pages that are the same for everyone, the way static generation works. Which one is up to the framework and your page.

What can cross from server to client

A Server Component and a Client Component run in different places, so props between them have to travel: React writes them into the payload as text and rebuilds them in the browser. Only values React knows how to write can cross. Here's a Server Component that hands a function to a Client Component:

Passing a click handler across the boundary (an error on purpose)Pop out in the code editor (opens in a new tab)JSX
// main.jsx: asks the pretend server for ProductPage, as a framework does for a page.import { showServerPage } from "./framework.jsx";import ProductPage from "./ProductPage.jsx";import AddToCart from "./AddToCart.jsx"; // A bundler finds the files that start with "use client" and lists them like this.const clientFiles = { "./AddToCart.jsx": AddToCart }; showServerPage(<ProductPage id="green-tea" />, clientFiles);
ResultLoading…

In ProductPage.jsx, line 12 passes onAdded, a function, to AddToCart. The result says The server stopped. Event handlers cannot be passed to Client Component props.: React's exact words. A function is code, and the server can't write code into the payload; the click would happen in the browser anyway. In the payload, "onAdded":"$2" points at row 2, 2:E…, an error row. The fix is to put the handler inside AddToCart, which runs where the click happens (exercise 3).

Try every kind of value. Each button passes a different tea prop to TeaCard, a Client Component:

Playground: what can cross to a Client Component?
Page.jsx: a Server ComponentJSX
import TeaCard from "./TeaCard.jsx";

export default function Page() {
  return <TeaCard tea="Green tea" />;
}
What React wrotecrosses
1:I["./TeaCard.jsx",["tea-card","tea-card.js"],"default"]
0:["$","$L1",null,{"tea":"Green tea"}]
Try it yourself
It crosses

Strings, numbers, booleans and null go into the row as they are.

TeaCard is a Client Component. Each case is the real output of React's server package, react-server-dom-webpack 19.3.1, for this code: the payload rows it wrote and, when it refused a value, the message it reported. The lesson's model writes the same rows, and its test checks every one.

The rule: plain data crosses. That's strings, numbers, booleans, null, undefined, arrays and plain objects of those, plus a few built-ins React handles, like Date, Map and promises, and JSX. Functions, objects made by a class, and symbols that only exist on the server don't. The one function that can cross is a Server Function, marked "use server": the browser gets a way to call it on the server. That's the next lesson, Server Functions. How to read the table: each row is something that can't cross, and what to do instead.

When a value can't cross
You want to passDo this instead
An event handler, like onAdded={() => …}Write the handler inside the Client Component, which runs in the browser where the click happens.
Any other functionPass the data the function needs, and call it on the side that has it. To call the server from the browser, use a Server Function, marked "use server".
An object made by a class, like new Money(4)Pass a plain object, like { amount: 4 }, and build what you need in the browser.
A database connection or a secretPass only the data you read with it. The connection and the secret stay on the server.

The other common mistake goes the opposite way: importing server-only code into a client file, such as a module that reads a secret. A client file's imports are sent to the browser, where anyone can read them. Next.js offers a guard for this:

payments.js: marked so it can't reach the browserJavaScript
// payments.js: code that must never reach the browser
import "server-only";

export async function chargeCard(orderId) {
  return fetch("https://payments.tea-shop.example/charge", {
    method: "POST",
    headers: { authorization: process.env.PAYMENTS_KEY },
    body: JSON.stringify({ orderId }),
  });
}

Line 2 imports the server-only package. In Next.js, if any client file imports payments.js, the build fails with an error, instead of the secret on line 7 quietly ending up in the browser.

Server Components inside Client Components

A Server Component can render Client Components, as ProductPage renders AddToCart. The other way round needs care: a Client Component can't import a Server Component, because everything a client file imports becomes client code. But it can show one that a Server Component passed in, usually as children. Here, the reviews sit inside a button that opens and closes them. Wait for the page, then click Show Reviews:

Server-rendered reviews inside a Client ComponentPop out in the code editor (opens in a new tab)JSX
// main.jsx: asks the pretend server for ProductPage, as a framework does for a page.import { showServerPage } from "./framework.jsx";import ProductPage from "./ProductPage.jsx";import AddToCart from "./AddToCart.jsx";import ShowMore from "./ShowMore.jsx"; // A bundler finds the files that start with "use client" and lists them like this.const clientFiles = { "./AddToCart.jsx": AddToCart, "./ShowMore.jsx": ShowMore }; showServerPage(<ProductPage id="green-tea" />, clientFiles);
ResultLoading…

In ProductPage.jsx, lines 5 to 14 are Reviews, an async Server Component that reads the reviews from the database. Lines 22 to 24 render ShowMore, a Client Component, with <Reviews /> inside it. In ShowMore.jsx, line 5 keeps open in state and line 11 shows children when it's open, inside a Suspense boundary of its own. ShowMore doesn't know or care what its children are: they arrive already rendered.

In the payload, ShowMore's children are "$L3": row 3, which comes about a second and a half after the others, when Reviews has its data. The page and its buttons work before that. If you open ShowMore before row 3 arrives, the $L3 in its children suspends, so ShowMore's boundary shows Loading Reviews... until the row comes. Without that boundary, the nearest one would be the whole page's.

Left, titled who renders what: ProductPage, then ShowMore under it, then Reviews under ShowMore, in its children slot. Right, titled who imports what: ProductPage.jsx imports ShowMore.jsx and holds Reviews itself; ShowMore.jsx imports nothing from Reviews.who renders whatProductPageShowMoreReviewsin ShowMore's childrenwho imports whatProductPage.jsxReviews (in this file)ShowMore.jsximports no Reviews
On the page, Reviews sits inside ShowMore. In the files, ProductPage renders Reviews and hands the result to ShowMore as children. ShowMore never imports Reviews, so Reviews stays a Server Component.

Here is the mistake to avoid: ShowMore importing Reviews itself.

A client file that imports a Server Component (a mistake)JSX
// ShowMore.jsx
"use client";
import { useState } from "react";
import Reviews from "./Reviews.jsx"; // Reviews becomes client code here

export default function ShowMore({ productId }) {
  const [open, setOpen] = useState(false);
  return (
    <section>
      <button onClick={() => setOpen(!open)}>Show reviews</button>
      {open && <Reviews productId={productId} />}
    </section>
  );
}

Line 4 imports Reviews into a file with "use client", so Reviews becomes client code. Its database code would be sent to the browser, and async components aren't supported there: React 19.3.1 prints <Reviews> is an async Client Component. Only Server Components can be async at the moment. in the console. Pass it as children instead, as above.

Context works the same way. A Server Component can't create context, but it can render a provider from a client file, around anything:

CartProvider.jsx: a context provider in a client fileJSX
// CartProvider.jsx
"use client";
import { createContext, useState } from "react";

export const CartContext = createContext(null);

export default function CartProvider({ children }) {
  const [items, setItems] = useState([]);
  return <CartContext value={{ items, setItems }}>{children}</CartContext>;
}
Layout.jsx: a Server Component renders the providerJSX
// Layout.jsx: a Server Component
import CartProvider from "./CartProvider.jsx";

export default function Layout({ children }) {
  return (
    <CartProvider>
      <header>Tea Shop</header>
      {children}
    </CartProvider>
  );
}

CartProvider keeps the cart in state (line 8) and provides it with context (line 9). Layout, a Server Component, renders it on lines 6 to 9, with the page as children. Every Client Component inside can read the cart, while the page around them stays on the server.

What the browser receives: the RSC payload

The rows you've been reading are the RSC payload: what the server sends the browser for a tree of Server Components. Here are the rows for the product page with reviews, as react-server-dom-webpack 19.3.1 wrote them (a production build; development builds add rows of debugging information):

The RSC payload for the product page with reviewsText
1:I["./AddToCart.jsx",["add-to-cart","add-to-cart.js"],"default"]
2:I["./ShowMore.jsx",["show-more","show-more.js"],"default"]
0:["$","main",null,{"children":[["$","h1",null,{"children":"Green tea"}],["$","$L1",null,{"productId":"green-tea","price":4}],["$","$L2",null,{"label":"Reviews","children":"$L3"}]]}]
3:["$","ul",null,{"children":[["$","li","Fresh and grassy",{"children":"Fresh and grassy"}],["$","li","Great value",{"children":"Great value"}]]}]

Each line is one row: a number, a colon, then the row. Line by line:

  1. Row 1 starts with I, for import. It names a Client Component: the file ./AddToCart.jsx, the chunk of JavaScript to download for it, and the export to use, default.
  2. Row 2 does the same for ShowMore.
  3. Row 0 is the page. Each ["$", type, key, props] is one element: "main" and "h1" are ordinary tags. Where a type is "$L1", the element is the component in row 1, with the props after it. ShowMore's children are "$L3", a row that hasn't arrived yet.
  4. Row 3 arrives later: the reviews list, rendered. The keys on the list items are in it.
Row 0, the main element with h1 Green tea, $L1, and $L2 with children $L3. Arrows lead from $L1 to row 1, an import of AddToCart.jsx, from $L2 to row 2, an import of ShowMore.jsx, and from $L3 to row 3, a ul with two reviews, sent later.row 0<main><h1>Green tea</h1>$L1 productId, price$L2 label: Reviewschildren: $L31: I AddToCart.jsx2: I ShowMore.jsx3: <ul> two reviewssent later
Row 0 is the page. Where a Client Component goes, it says $L1 or $L2: the component named in that import row. Where the reviews go, it says $L3: a row the server sends later, when Reviews has its data.

Notice what isn't there: no ProductPage, no Reviews, no db. Server Components leave only their output. The rows also come in the order they're ready, which is why row 0 comes after rows 1 and 2, and row 3 last. The browser shows what it has and fills in the rest as it arrives, like Suspense does with data.

Animation: one request, row by row
Step 1 of 5
Request

The browser asks for the product page. The server starts rendering ProductPage.

On the left, the server, with what it's doing now. In the middle, the rows the browser has received. On the right, the page: empty, then the product with its buttons, then the reviews in ShowMore.serverProductPageReviewsProductPage startsrows receivedpage
Press Play, or Step through one request. The rows are the real ones from the payload above.

The payload isn't HTML. For the very first load, a framework also turns it into HTML on the server, so the page shows before any JavaScript runs, and then hydrates the Client Components. On later navigations, it can fetch just the payload for the next page and update what's on screen, keeping the state of Client Components that stay. How to read the table: each row compares server rendering, from the earlier lessons, with Server Components.

Server rendering and Server Components
QuestionServer rendering (SSR)Server Components
What does it make?HTML for the first load of a page.The RSC payload: rows describing the rendered tree, with references to Client Components.
Is the component's code sent to the browser?Yes: the browser runs every component again to hydrate the HTML.No: only Client Components' code is sent.
When does it run?Once per page load.On the first load and again whenever the app asks the server for fresh content, such as on navigation.
Do you choose one?Frameworks use both: Server Components make the payload, and server rendering turns it into the first HTML.Frameworks use both: Server Components make the payload, and server rendering turns it into the first HTML.

If you'd like to see how this lesson's model writes and reads rows, here it is. You don't need it for the rest of the lesson. renderToPayload (lines 11 to 119) is the server half: line 105 calls a Server Component, line 107 waits for an async one at the top of a row, and lines 108 to 110 give one further down a row of its own, $L. Line 61 writes an import row. createResponse (lines 123 to 188) is the browser half: line 136 suspends until a row arrives, with use.

mini-rsc.js: the lesson's model of the server and browser halvesJavaScript
// mini-rsc.js: a small model of React Server Components, written for this lesson. It isn't React's
// code, and it only knows the values this lesson uses. For the lesson's examples it writes the same
// rows that React's real server package, react-server-dom-webpack 19.3.1, wrote for them.
import { createElement, use } from "react";

const ELEMENT = Symbol.for("react.transitional.element");
const FRAGMENT = Symbol.for("react.fragment");

// ---- The server half: run the Server Components, write the payload one row at a time ----------

export function renderToPayload(element, clientFiles, { onRow, onError }) {
  const fileOf = new Map(Object.entries(clientFiles).map(([file, component]) => [component, file]));
  const importRows = new Map();
  const ready = { imports: [], models: [], errors: [] };
  let nextId = 1;
  let stopped = false;
  let flushing = false;

  // Rows that are ready go out together: Client Component imports first, then models, then errors.
  function flush() {
    if (flushing) return;
    flushing = true;
    setTimeout(() => {
      flushing = false;
      if (stopped) return;
      const rows = [...ready.imports, ...ready.models, ...ready.errors];
      ready.imports = [];
      ready.models = [];
      ready.errors = [];
      for (const row of rows) onRow(row);
    });
  }

  function fail(message) {
    const id = nextId++;
    if (!stopped) onError(message);
    ready.errors.push(`${id}:E{"digest":""}`);
    return `$${id}`;
  }

  // One row: a value to serialize. A Server Component that awaits at the top of a row waits there.
  async function task(id, value) {
    while (true) {
      try {
        ready.models.push(`${id}:${JSON.stringify(toJSON(value, true))}`);
        flush();
        return;
      } catch (thrown) {
        if (!thrown?.waitFor) throw thrown;
        value = await thrown.waitFor;
      }
    }
  }

  function clientReference(component) {
    if (!importRows.has(component)) {
      const id = nextId++;
      const file = fileOf.get(component);
      const chunk = file.replace(/^\.\//, "").replace(/\.jsx?$/, "").replace(/(?<=[a-z])(?=[A-Z])/g, "-").toLowerCase();
      importRows.set(component, id);
      ready.imports.push(`${id}:I${JSON.stringify([file, [chunk, `${chunk}.js`], "default"])}`);
    }
    return `$L${importRows.get(component)}`;
  }

  function toJSON(value, atTop = false, isClientProp = false, name = "") {
    if (value === undefined) return "$undefined";
    if (value === null || typeof value === "number" || typeof value === "boolean") return value;
    if (typeof value === "string") return value.startsWith("$") ? `$${value}` : value;
    if (typeof value === "function") {
      return fail(
        /^on[A-Z]/.test(name) && isClientProp
          ? "Event handlers cannot be passed to Client Component props."
          : 'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". Or maybe you meant to call this function rather than return it.',
      );
    }
    if (typeof value === "symbol") return fail(`Only global symbols received from Symbol.for(...) can be passed to Client Components. The symbol Symbol.for(${value.description}) cannot be found among global symbols.`);
    if (typeof value.then === "function") {
      const id = nextId++;
      value.then((result) => task(id, result));
      return `$@${id}`;
    }
    if (Array.isArray(value)) return value.map((item) => toJSON(item));
    if (value instanceof Date) return `$D${value.toISOString()}`;
    if (value instanceof Map) {
      const id = nextId++;
      ready.models.push(`${id}:${JSON.stringify(toJSON([...value]))}`);
      return `$Q${id}`;
    }
    if (value.$$typeof === ELEMENT) return elementToJSON(value, atTop);
    if (Object.getPrototypeOf(value) !== Object.prototype) {
      return fail("Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported.");
    }
    return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, toJSON(item, false, isClientProp, key)]));
  }

  function elementToJSON({ type, key, props }, atTop) {
    if (type === FRAGMENT) return toJSON(props.children);
    if (fileOf.has(type)) {
      const reference = clientReference(type);
      return ["$", reference, key, toJSON(props, false, true)];
    }
    if (typeof type === "function") {
      // A Server Component: call it here, on the server.
      const result = type(props);
      if (typeof result?.then !== "function") return toJSON(result, atTop);
      if (atTop) throw { waitFor: result };
      const id = nextId++;
      result.then((output) => task(id, output));
      return `$L${id}`;
    }
    return ["$", type, key, toJSON(props)];
  }

  task(0, element);
  return () => {
    stopped = true;
  };
}

// ---- The browser half: turn rows back into React elements --------------------------------------

export function createResponse(clientFiles) {
  const rows = new Map();

  function row(id) {
    if (!rows.has(id)) {
      let resolve;
      const promise = new Promise((done) => (resolve = done));
      rows.set(id, { promise, resolve, value: undefined, ready: false });
    }
    return rows.get(id);
  }

  function RowContent({ id }) {
    return use(row(id).promise);
  }

  function fromJSON(value) {
    if (typeof value === "string") {
      if (!value.startsWith("$")) return value;
      if (value.startsWith("$$")) return value.slice(1);
      if (value === "$undefined") return undefined;
      if (value.startsWith("$D")) return new Date(value.slice(2));
      if (value.startsWith("$@")) return row(Number(value.slice(2))).promise;
      if (value.startsWith("$Q")) return new Map(row(Number(value.slice(2))).value);
      if (value.startsWith("$L")) {
        const target = row(Number(value.slice(2)));
        if (target.ready && target.isImport) return target.value;
        return createElement(RowContent, { id: Number(value.slice(2)) });
      }
      return value;
    }
    if (Array.isArray(value)) {
      if (value[0] === "$" && value.length === 4) {
        const [, type, key, props] = value;
        const { children, ...rest } = fromJSON(props);
        if (key !== null) rest.key = key;
        return Array.isArray(children) ? createElement(fromJSON(type), rest, ...children) : createElement(fromJSON(type), rest, children);
      }
      return value.map(fromJSON);
    }
    if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, fromJSON(item)]));
    return value;
  }

  return {
    root: createElement(RowContent, { id: 0 }),
    receive(text) {
      const [, id, tag, json] = /^(\d+):([A-Z]?)(.*)$/.exec(text);
      const target = row(Number(id));
      const data = JSON.parse(json);
      if (tag === "I") {
        target.isImport = true;
        target.value = clientFiles[data[0]];
      } else if (tag === "E") {
        target.value = new Error("The server couldn't send this value.");
      } else {
        target.value = fromJSON(data);
      }
      target.ready = true;
      // Like React's own rows, the promise says it's done, so use() can read it without waiting.
      target.promise.status = "fulfilled";
      target.promise.value = target.value;
      target.resolve(target.value);
    },
  };
}

React's real format has more kinds of row and more details than this model covers. The lesson Server Components inside: the Flight protocol reads it in React's source.

Splitting a shop page between server and client

Start with everything as a Server Component, and add "use client" only where the page needs the browser: state, Effects, event handlers, or browser-only things like localStorage. Put the directive as low in the tree as you can, on the small interactive parts, so the big parts stay on the server. A product page might end up like this:

  • Server: the page itself, the product details, the reviews, the related products, the footer. They read data and render.
  • Client: the Add to cart button, the image carousel, the size picker, the Show reviews toggle.

Server Components help most on pages with a lot of content and a little interaction, like a shop, a blog or documentation. An app that's interactive everywhere, like a drawing tool, gains less: most of it must run in the browser anyway.

Server Component or Client Component?
  • Reads the product from the database with await
  • An Add to cart button with an onClick
  • A menu that opens and closes with useState
  • Turns a long description into HTML with a big Markdown library
  • Reads the saved theme from localStorage
  • The shop's footer: links that never change
  • A search box that filters the list as you type
  • Calls a payment API with a secret key
  • A countdown that ticks every second with useEffect
Try it yourself
0 of 9 correct

Sort each part of a shop: should it be a Server Component, or does it need to be a Client Component?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "'use server' marks a Server Component." No. Server Components have no directive. "use server" marks Server Functions, the next lesson.
  • "Server Components are server-side rendering." No. SSR makes HTML and still sends every component's code. Server Components send a payload and keep their code on the server. Frameworks use both.
  • "Client Components only run in the browser." No. Frameworks also render them on the server for the first HTML. They're the components whose code is sent to the browser.
  • "Every component under a Client Component must have 'use client'." No. The directive marks a file and everything it imports. Components passed in as children can still be Server Components.
  • "A Server Component can pass an onClick to a Client Component." No. Functions can't be written into the payload. Put the handler in the Client Component.
  • "Server Components need a server running all the time." No. They can run once, when the app is built, for pages that are the same for everyone.

Practice exercises

Each exercise runs on the lesson's model, so the result shows the page and the payload. Press Edit & run, open the tab the task names, and change it.

Exercise 1 · Warm-upShow the price, on the server

Add a line that shows the price under the description. What does it show?

// main.jsx: asks the pretend server for ProductPage, as a framework does for a page.import { showServerPage } from "./framework.jsx";import ProductPage from "./ProductPage.jsx";import AddToCart from "./AddToCart.jsx"; // A bundler finds the files that start with "use client" and lists them like this.const clientFiles = { "./AddToCart.jsx": AddToCart }; showServerPage(<ProductPage id="green-tea" />, clientFiles);
ResultLoading…

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPredict what the browser downloads

    This is ProductPage.jsx from the overview. Which file's code does the browser download for the page's components?

    import { db } from "./db.js";
    import AddToCart from "./AddToCart.jsx";
    
    export default async function ProductPage({ id }) {
      const product = await db.products.find(id);
      return (
        <main>
          <h1>{product.name}</h1>
          <p>{product.description}</p>
          <AddToCart productId={product.id} price={product.price} />
        </main>
      );
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeMove the handler to the client

      Fix the page so it renders, and AddToCart still logs added when clicked. How many rows does the payload have then?

      // main.jsx: asks the pretend server for ProductPage, as a framework does for a page.import { showServerPage } from "./framework.jsx";import ProductPage from "./ProductPage.jsx";import AddToCart from "./AddToCart.jsx"; // A bundler finds the files that start with "use client" and lists them like this.const clientFiles = { "./AddToCart.jsx": AddToCart }; showServerPage(<ProductPage id="green-tea" />, clientFiles);
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticePut the reviews in the slot

        ShowMore opens, but shows nothing. Give it the reviews, without touching ShowMore.jsx. Which row of the payload arrives last?

        // main.jsx: asks the pretend server for ProductPage, as a framework does for a page.import { showServerPage } from "./framework.jsx";import ProductPage from "./ProductPage.jsx";import AddToCart from "./AddToCart.jsx";import ShowMore from "./ShowMore.jsx"; // A bundler finds the files that start with "use client" and lists them like this.const clientFiles = { "./AddToCart.jsx": AddToCart, "./ShowMore.jsx": ShowMore }; showServerPage(<ProductPage id="green-tea" />, clientFiles);
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeSend a real Date

          Restock says not a Date, because ProductPage passes a string. Pass the Date itself. What does the payload write for it?

          // main.jsx: asks the pretend server for ProductPage, as a framework does for a page.import { showServerPage } from "./framework.jsx";import ProductPage from "./ProductPage.jsx";import Restock from "./Restock.jsx"; // A bundler finds the files that start with "use client" and lists them like this.const clientFiles = { "./Restock.jsx": Restock }; showServerPage(<ProductPage id="green-tea" />, clientFiles);
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeMove the boundary down

            The whole panel is a Client Component, so its code, and the whole product, go to the browser. Make only the button a Client Component. Which Client Component file does the browser load then?

            // main.jsx: asks the pretend server for ProductPage, as a framework does for a page.import { showServerPage } from "./framework.jsx";import ProductPage from "./ProductPage.jsx";import TeaPanel from "./TeaPanel.jsx"; // A bundler finds the files that start with "use client" and lists them like this.const clientFiles = { "./TeaPanel.jsx": TeaPanel }; showServerPage(<ProductPage id="green-tea" />, clientFiles);
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask where the code runs, and what has to travel between server and browser.

              The Server Components quiz · 8 questionsScore: first tries count
              1. Question 1 of 8How do you mark a component as a Client Component?

                Choose an answer to see the explanation.

              2. Question 2 of 8TeaRow is a Client Component. What happens when this Server Component renders?

                Read the code, then predictJSX
                // ProductList.jsx: a Server Component
                import TeaRow from "./TeaRow.jsx"; // "use client"
                
                export default async function ProductList() {
                  const teas = await db.teas.all();
                  return teas.map((tea) => (
                    <TeaRow key={tea.id} tea={tea} onSelect={() => console.log(tea.id)} />
                  ));
                }

                Choose an answer to see the explanation.

              3. Question 3 of 8Which prop can a Server Component pass to a Client Component?

                Choose an answer to see the explanation.

              4. Question 4 of 8ProductPage is a Server Component. Which files' code does the browser download for these components?

                Read the code, then predictJavaScript
                // ProductPage.jsx: no directive
                import AddToCart from "./AddToCart.jsx";
                
                // AddToCart.jsx
                "use client";
                import { formatPrice } from "./formatPrice.js";

                Choose an answer to see the explanation.

              5. Question 5 of 8In this payload, what does $L1 in row 0 mean?

                Read the code, then predictText
                1:I["./AddToCart.jsx",["add-to-cart","add-to-cart.js"],"default"]
                0:["$","$L1",null,{"productId":"green-tea","price":4}]

                Choose an answer to see the explanation.

              6. Question 6 of 8ShowMore is a Client Component. How can it show the server-rendered Reviews?

                Choose an answer to see the explanation.

              7. Question 7 of 8How do Server Components differ from server-side rendering (SSR)?

                Choose an answer to see the explanation.

              8. Question 8 of 8Can a Server Component call useState?

                Choose an answer to see the explanation.

              Key takeaways

              • Server Components run only on the server, per request or at build time. They can be async and read data directly, and their code never reaches the browser.
              • "use client" at the top of a file makes it, and everything it imports, client code. Client Components handle state, Effects and events. There is no directive for Server Components.
              • Props from a Server Component to a Client Component must be data React can write: plain values, arrays, plain objects, Dates, Maps, promises and JSX. Functions can't cross, except Server Functions.
              • A Client Component can't import a Server Component, but it can show one passed in as children or another prop.
              • The RSC payload describes the rendered tree in rows, with references to Client Components and to rows that come later. Frameworks also turn it into HTML for the first load.

              Remember the one-liner.
              Render on the server what doesn't need the browser, and send only the result.

              Coming next: Server Functions. With "use server", the browser can call a function that runs on the server, from a form or a click.

              CompleteFrontend Clear concepts. Working examples.