Skip to content

    Frontend System Design

    React Server Components

    Understand what React Server Components are, what runs on the server and in the browser, how the use client boundary and payload work, and when to use them.

    By Published 15 min read

    • frontend-system-design
    • react-server-components
    • use-client
    • react
    • rendering
    Share

    Flagship lessonadvancedReact labFrontend System Design

    Before this: Hydration, Selective Hydration and Islands, React Reconciliation and the Virtual DOM

    What you'll learn: By the end of this lesson you will be able to explain what a React Server Component is and why it exists, separate "where does this component's code run" from "how was this page's HTML produced", place the "use client" boundary on the module graph and predict what crosses it, describe what the server sends to the browser (as the React and Next.js documentation describe it), and decide which parts of a page should be Server Components, Client Components or neither.

    A restaurant sends dishes to your table. It does not send the recipes, the knives or the supplier's phone number. You get the result of the cooking, and anything that needs you, such as a pepper mill you turn yourself, is brought to the table as an object you operate.

    React Server Components apply that split to a component tree. Some components are "cooked" ahead of time in a separate environment, and the browser receives what they produced, not the code that produced it. Other components, the ones a visitor operates, are sent as code and run in the browser. The hard parts are knowing which is which, and what may pass between them.

    The running example is the trail-shoe product page from Hydration, Selective Hydration and Islands: a gallery and description read from a database, a reviews section that loads slowly, and an "Add to cart" button with state.

    Nothing in this lesson was run

    React Server Components need a bundler and a framework that understand them. Unlike the lessons before it, this one has no lab: the project's lab environment (Node and a DOM model) cannot run a framework and bundler honestly, and an invented simulation would teach the wrong thing. Everything below is explained from the sources opened for it: the react.dev pages on Server Components, "use client" and Server Functions, the React 19 announcement, the original RFC, and the Next.js documentation. Code samples are adapted from those pages, marked as such, and have not been run. Where a statement is about Next.js and not React, the text says so. At the time of writing, react.dev lists React 19.3 as the latest version and the Next.js pages read were last updated in October and November 2026.

    The core idea

    1. A Server Component renders ahead of time, before bundling, in an environment separate from the browser. react.dev describes it as "a new type of Component that renders ahead of time, before bundling, in an environment separate from your client app or SSR server". That environment can be a build step or a web server handling a request.
    2. The browser receives the output, not the code. Libraries a Server Component imports stay out of the client bundle.
    3. A Client Component is any component whose module is on the client side of a "use client" boundary. It is sent as code, hydrated and interactive.
    4. The boundary is drawn on the module dependency tree, not on the render tree, and only serialisable data can cross it as props.
    5. This is not server-side rendering. SSR answers "how was the HTML produced". Server Components answer "where does this component's code run".

    RSC is stable in React 19, but the APIs for building a bundler or framework "do not follow semver and may break between minors in React 19.x", so application developers use it through a framework.

    Why it exists

    The original RFC (number 0188 in the React RFCs repository) listed three motivations. Zero-bundle-size components: server-only packages can be used "incurring zero impact on bundle size". Full access to the backend: without it, teams "have to expose additional endpoints to power their UI". Automatic code splitting: the RFC says Server Components "treat all imports of Client Components as potential code-split points", so you need no hand-written React.lazy. The RFC states the price: a Server Component "may not use state, because they execute (conceptually) only once per request", may not use effects, and may not use browser-only APIs.

    Hydration shows why this matters: Hydration, Selective Hydration and Islands found that selective hydration reorders work but every component still runs in the browser, static ones included. A Server Component removes a component from the browser entirely.

    Two questions that get confused

    Next.js documents the distinction with a table. A Server Component runs on the server and not in the browser. A Client Component runs on the server (to produce the HTML) and in the browser. In its words: "Rendering a Client Component on the server produces HTML, but the component remains a Client Component", and "'Server-rendered' describes how Next.js produced the HTML. 'Server Component' describes where the component code runs and whether that code ships to the browser."

    Runs on the server Runs in the browser Code sent to the browser
    Server Component yes no no
    Client Component yes (to prerender HTML) yes (hydration, updates) yes

    So a page can be fully server-rendered HTML and still have Client Components in it, and a Server Component is not "SSR with a new name".

    Figure 1. A product page split by the boundary

    Server Components on one side of the use client boundary, one Client Component on the otherProduct page to Gallery, description (output); Product page to Reviews (output, later); Product page to Add to cart (props, reference)SERVER: CODE STAYS HERECLIENT: CODE IS SENTProduct page: Server ComponentProduct pageServer ComponentGallery, description: Server ComponentGallery,descriptionServer ComponentReviews: async Server ComponentReviewsasync ServerComponentAdd to cart: Client ComponentAdd to cartClient Componentoutputoutput, laterprops, reference

    How to read this: the page, the gallery and the reviews run only on the server, and the browser gets what they rendered. The cart button has state and a click handler, so it is a Client Component: the server sends it a reference to its code, plus its props.

    What a Server Component can do

    A Server Component can be an async function. react.dev: "When you await in an async component, React will suspend and wait for the promise to resolve before resuming rendering", and async components are "not supported on the client". Because it never reaches the browser, it can read the database directly, without an API in between. Adapted from the react.dev page (not run here):

    import db from './database';
    
    async function Note({id}) {
      const note = await db.notes.get(id);
      return (
        <div>
          <Author id={note.authorId} />
          <p>{note.content}</p>
        </div>
      );
    }

    react.dev also says Server Components can run at build time, "to read from the filesystem or fetch static content, so a web server is not required", or per request on a web server. For the build-time case the page says the client sees only the HTML, not the component or its libraries.

    What it cannot do: "Server Components are not sent to the browser, so they cannot use interactive APIs like useState." The "use client" reference adds that they "cannot support interaction as event handlers must be registered and triggered by a client", and Next.js adds that context is not supported in Server Components. There is no directive for Server Components: "There is no directive for Server Components." A component is a Server Component unless it is on the client side of the boundary.

    The "use client" boundary

    Adding "use client" at the top of a file "marks the module and its transitive dependencies as client code", and creates "a server-client boundary in the module dependency tree". The reference is explicit that the boundary is on "the module dependency tree, not the render tree". Three consequences:

    • Everything the file imports becomes client code. A component is a Client Component "if it is defined in module with 'use client' directive or when it is a transitive dependency of a module that contains a 'use client' directive". Next.js advises adding the directive on the files that define interactive components, rather than marking large parts of the UI.
    • You only mark the entry. The directive "has no effect" in a module already imported from client code.
    • One module can run in both places. "A single module may be evaluated on the server when imported from server code and on the client when imported from client code."

    Adapted from react.dev (not run here):

    // Client Component
    "use client"
    
    export default function Expandable({children}) {
      const [expanded, setExpanded] = useState(false);
      // ...
    }

    What can cross the boundary

    Props from a Server Component to a Client Component must be serialisable. react.dev lists primitives, iterables of serialisable values (arrays, maps, sets, typed arrays), Date, plain objects, Server Functions, elements of Server or Client Components, and promises. Functions that are not exported from client-marked modules or marked 'use server', classes, instances of non-built-in classes and unregistered symbols are not supported. Next.js: "Passing a function as a prop from a Server Component to a Client Component throws. An event handler like onClick cannot cross."

    Server Components inside Client Components

    A Client Component cannot import a Server Component (that would pull its code into the client graph), but it can receive one as children. react.dev: "a parent-child render relationship between components does not guarantee the same render environment." Next.js shows a Modal (client, with open and close state) around a Cart (server, fetches data): Page owns both, Modal is only the parent of Cart, so Cart renders on the server and Modal receives its output. The Next.js page defines the owner as the component whose source contains the JSX for a child, and the parent as the component that contains it in the rendered tree. Rendering a Server Component's output inside a Client Component works; importing it does not.

    What goes over the wire

    The documentation does not hand you a byte format to memorise, and this lesson does not invent one. Next.js describes the RSC Payload as "a compact, serialized representation of the rendered React Server Components tree". It contains the rendered result of Server Components, "placeholders for where Client Components should be rendered and references to their JavaScript files", and any props passed from Server to Client Components.

    Figure 2. First load, as the Next.js documentation describes it

    sequenceDiagram accTitle: First load of a page with Server and Client Components in Next.js accDescr: On the server, Server Components render into the RSC payload. Client Components and the payload are used to prerender HTML. In the browser, the HTML is shown first, the payload reconciles the client and server trees, and the JavaScript hydrates the Client Components. participant B as Browser participant S as Framework server participant C as Server Components B->>S: Request the page S->>C: Render the Server Components C-->>S: RSC payload S->>S: Prerender HTML (payload and Client Components) S-->>B: HTML, with the payload embedded B->>B: Show the HTML (non-interactive preview) B->>B: Use the payload to reconcile the trees B->>B: Hydrate the Client Components with their JavaScript

    How to read this: the HTML gives a fast, non-interactive preview, the payload keeps the browser's component tree in step with the server's, and the JavaScript hydrates only the Client Components. This is Next.js's description; other frameworks may differ.

    On later navigations Next.js documents that the payload is prefetched and cached, and Client Components render in the browser without server HTML. The streaming guide adds that on client-side navigation only the component payload is fetched, with an rsc: 1 request header, "and no HTML is transferred at all", and that on the initial load a Suspense boundary's HTML arrives together with an inline script carrying the payload "so React can later hydrate it". The server and client boundary guide adds that mutating Server Component DOM nodes directly can put the DOM out of sync with React's tree.

    Streaming data to the client

    A Server Component can start work and pass the unresolved promise down. react.dev: "You can even create a promise on the server, and await it on the client." Adapted from react.dev (not run here):

    // Server Component
    async function Page({id}) {
      const note = await db.notes.get(id);           // awaited on server
      const commentsPromise = db.comments.get(note.id); // started, not awaited
      return (
        <div>
          <p>{note.content}</p>
          <Suspense fallback={<p>Loading Comments...</p>}>
            <Comments commentsPromise={commentsPromise} />
          </Suspense>
        </div>
      );
    }

    The page explains that the note is needed for the first render, so it is awaited on the server, while the comments are lower priority and are read in a Client Component with use. The Suspense boundary shows its fallback meanwhile. How this combines with streaming HTML is covered in Streaming SSR and Suspense.

    Server Functions are a different thing

    "use server" marks a Server Function: an async function a Client Component can call, executed on the server. react.dev: when called on the client, "React will send a request to the server to execute the function, and return the result", and on the client the function is a reference, not the implementation. They are not Server Components and have their own concerns (validation, authorisation of every call), which this lesson does not cover.

    Choosing: server or client

    Figure 3. Server Component or Client Component

    flowchart TD accTitle: Choosing between Server and Client Components accDescr: If a component needs state, event handlers, effects, browser-only APIs or custom hooks, it is a Client Component. If it reads data close to the source, uses secrets, or has libraries that should not reach the browser, it is a Server Component. Otherwise default to a Server Component and keep Client Components small and low in the tree. A["Does the component need
    state, event handlers,
    effects or browser APIs?"] -->|"yes"| B["Client Component:
    mark the file 'use client'"] A -->|"no"| C["Server Component (default)"] B --> D["Keep it small and low
    in the tree; pass server
    output as children"] C --> E["Read data and secrets here;
    keep big libraries out
    of the browser"]

    How to read this: the question is not "does this page need to be dynamic" but "does this piece need to run in the browser". Everything that does not is a candidate for the server.

    Next.js lists the reasons to choose each: Client Components for state and event handlers, lifecycle logic, browser-only APIs and custom hooks; Server Components for fetching data close to the source, using secrets without exposing them, reducing the JavaScript sent to the browser and streaming content progressively.

    Approach JavaScript in the browser Data access Costs
    Client-side rendering all components through an API content appears only after JavaScript runs
    SSR plus hydration all components (again, to hydrate) server renders, client needs the data too the full hydration cost
    Islands only the islands per island separate roots, shared state mechanism
    Server Components only Client Components directly, on the server a framework and bundler, the boundary rules, serialisable props

    Three illustrations

    Illustration 1, the minimal example. An async Note that awaits the database and renders markup (the sample above). The browser receives its output; the database client is never shipped.

    Illustration 2, the realistic example. The product page of Figure 1. The page, gallery, description and reviews are Server Components. AddToCart, with state and a click handler, is a Client Component, and the server passes it the product id as a prop. In the terms of the hydration lesson, the gallery, the description and the reviews no longer run in the browser at all.

    Illustration 3, the failure and edge case. Next.js documents two ways the boundary bites. Pass a function prop from a Server Component to a Client Component and it throws. And a compound component such as Menu.Item, which hangs subcomponents off a function as static properties, breaks when a Server Component imports it from a client module: it receives a client reference rather than the function, Menu.Item is undefined, and React throws "Element type is invalid". The documented fix is to expose the pieces as named exports.

    In practice: moving a read-only reviews section to the server

    This is a realistic scenario, not a case history.

    The situation

    A shop's product page is a single client-rendered tree that is server-rendered and hydrated. The reviews section renders review text through a rich-text library and a date library, calls an internal reviews API from the browser after load, and has one interactive piece: a "Helpful" button. The product team wants less JavaScript on the page and no extra API round trip.

    Starting architecture

    Figure 4. Reviews today: the browser calls an API

    Reviews rendered in the browser from an API, with libraries in the bundleReviews store to Reviews API (rows); Reviews API to Reviews UI (JSON, after load); Client bundle to Reviews UI (code)BROWSERReviews store: databaseReviews storedatabaseReviews API: internal serviceReviews APIinternal serviceClient bundle: page, librariesClient bundlepage, librariesReviews UI: renders in the browserReviews UIrenders in thebrowserrowsJSON, after loadcode

    How to read this: the libraries that format reviews are in the client bundle, and the data arrives in a second request after the page loads.

    Options considered

    Option Result Risk
    Keep client rendering, split the chunk fewer bytes on first load the API round trip and the libraries remain
    Islands only the helpful button hydrates a loader to maintain, separate roots
    Server Components formatting libraries stay on the server; data read directly needs a framework that supports it; boundary rules

    The decision

    Make the reviews section a Server Component that reads the store directly and formats on the server, with the Helpful button as a small Client Component that receives the review id. Keep the page's existing framework features; adopt this only on a framework that supports Server Components. Revisit if per-request server work becomes a cost problem.

    Rollout and verification

    1. Move one section first. Check with the bundler's analysis tool that the libraries no longer appear in the client bundle.
    2. Review every prop that crosses into a Client Component: it must be serialisable, and Next.js notes that props are serialised and sent to the browser, so nothing secret may be passed.
    3. Add the server-only package to modules that read the store, so an accidental client import fails the build.
    4. Compare the page's behaviour with JavaScript slow or failing: the section should still render.

    The 2 a.m. failure

    A developer imports the data module into a Client Component to reuse a helper. Next.js documents what happens: environment variables that are not prefixed NEXT_PUBLIC_ are replaced with an empty string in the client bundle, so the function "can be imported and executed on the client" but "won't work as expected". The reviews call fails only in the browser, with an authorisation error from the empty key. The fix is to move the helper into a shared module without server code and to mark the data module server-only, which turns the mistake into a build error.

    Architect's notes

    • Start with the boundary map. Decide per component whether it needs the browser; most of a content page does not.
    • Keep Client Components small and low. The directive pulls in everything the file imports.
    • Treat props as a public API. Anything passed to a Client Component is serialised and sent to the browser.
    • Prefer children over imports to nest server output inside client shells.
    • Check the framework, not just React. The React docs say the bundler and framework APIs may change between minors; the Next.js behaviour in this lesson is Next.js's.
    • Do not adopt it for one widget. The benefit is largest on pages that are mostly read-only.

    Common mistakes

    • Equating Server Components with SSR. Client Components are also rendered on the server.
    • Marking a layout "use client" to use one hook, which makes its whole import subtree client code.
    • Passing functions, classes or secrets as props.
    • Importing server code into client files, where non-public environment variables are empty.
    • Using static properties on client components from a Server Component.
    • Calling useState in a Server Component.

    Key takeaways

    • A Server Component renders ahead of time in a separate environment; the browser receives its output, not its code.
    • "Server Component" is about where code runs; "server-rendered" is about how HTML was produced.
    • "use client" draws a boundary on the module graph; imports below it become client code.
    • Only serialisable props, elements and promises cross the boundary; functions and classes do not.
    • Client Components are still rendered on the server and are hydrated in the browser.
    • The RSC payload carries rendered output, placeholders and props, as Next.js documents it.
    • RSC is stable in React 19, but the bundler and framework APIs are not semver-stable: use a framework.
    • Server Functions are different from Server Components.

    Practice exercises

    Exercise 1, easy (20 minutes). List the components of a page you know and mark each Server or Client. For each Client Component, name the state, handler or browser API that forces it.

    Exercise 2, intermediate (45 minutes). In Figure 1, move the reviews' "Helpful" button inside the reviews. Draw the new boundary and say what the reviews component passes to the button.

    Exercise 3, intermediate (60 minutes). Read the Next.js server and client boundary guide and write down which of your imports would pull server-only code into the client graph.

    Exercise 4, advanced (2 hours). Compare, for the product page, one root with Suspense boundaries, islands and Server Components. State what each runs in the browser and what you would measure.

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments