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
- 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.
- The browser receives the output, not the code. Libraries a Server Component imports stay out of the client bundle.
- 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. - The boundary is drawn on the module dependency tree, not on the render tree, and only serialisable data can cross it as props.
- 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
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
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
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
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
- Move one section first. Check with the bundler's analysis tool that the libraries no longer appear in the client bundle.
- 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.
- Add the
server-onlypackage to modules that read the store, so an accidental client import fails the build. - 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
useStatein 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.
Related reading
- Hydration, Selective Hydration and Islands: the cost Server Components avoid for some components.
- Debugging Hydration Mismatches: what goes wrong in the Client Components that still hydrate.
- Streaming SSR and Suspense: streaming and boundaries.
- Choosing a Rendering Strategy per Page: the per-page decision.
- React Reconciliation and the Virtual DOM: how the client tree is reconciled.
Official documentation
- Server Components (react.dev): definition, build-time and request-time rendering, async components.
- 'use client' (react.dev): the boundary, transitive dependencies, serialisable props.
- Server Functions (react.dev):
"use server"and calling server code from the client. - React v19 (react.dev): Server Components stable, the semver caveat.
- Server and Client Components (Next.js): the RSC payload, first load, when to use each.
- The Server and Client Boundary (Next.js): rendering environments, crossing the boundary, owner and parent.