Topic 20.2
React Server Components
In one line
React Server Components run only on the server (at build time or per request), can be async and read data directly, and send their result to the browser instead of their code; "use client" marks where client components, which have state and event handlers, begin.
Think of it like this
A restaurant kitchen and the dining table. The kitchen (the server) has the fridge, the knives and the secret recipes; it cooks and sends out finished plates. The table (the browser) gets the plate, plus a few tools you use while eating: salt, a fork, a sauce you pour yourself. Nobody ships the whole kitchen to your table. Server Components are the cooking done in the kitchen; Client Components are the tools on the table.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Server Component
- A component that runs only on the server; its code never reaches the browser, only what it renders.
- Client Component
- A normal interactive component that runs in the browser and can use state, effects and event handlers.
- "use client"
- A directive at the top of a file that marks it, and everything it imports, as client code.
- Boundary
- The line in the module graph where server code stops and client code starts.
- RSC payload
- The serialized result of rendering server components, sent to the browser to build or update the page.
- Serializable
- Can be turned into text and rebuilt on the other side: data yes, functions no.
- Bundle
- The JavaScript files the browser downloads to run your app.
- Module graph
- The tree of files formed by following every
importfrom an entry file.
Step by step
01Start with a server component
In a Next.js App Router project, app/page.tsx is a server component unless it says otherwise. It can be async and talk to the database directly.
import { db } from "@/lib/db";
import LikeButton from "./like-button";
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await db.post.findUnique({ where: { id } }); // runs on the server only
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<LikeButton postId={post.id} initialLikes={post.likes} />
</article>
);
}02Add an interactive island with "use client"
The like button needs state and a click handler, so it lives in its own file that starts with "use client". The page passes it plain data (an id and a number).
"use client";
import { useState } from "react";
export default function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes);
return <button onClick={() => setLikes(likes + 1)}>Like ({likes})</button>;
}03What runs where
On the server, React renders Page (running its query) and records LikeButton as a reference with its props. The server also pre-renders the client component to HTML for the first load. In the browser, only LikeButton's code is downloaded and hydrated.
04Pass server UI into client components as children
A client Tabs or Modal can't import a server component, but a server parent can pass one in as children. It is rendered on the server, and the client component only places the result.
import Modal from "./ui/modal"; // "use client"
import Cart from "./ui/cart"; // server component, reads the database
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
);
}05Keep server-only code out of the client
Mark modules that must never reach the browser (database clients, secret-using helpers) with import "server-only". Importing such a module from a client component then fails the build instead of leaking. The client-only package does the reverse for code that touches window.
import "server-only";
export async function getData() {
const res = await fetch("https://api.example.com/data", {
headers: { authorization: process.env.API_KEY! },
});
return res.json();
}Try it yourself
- 1
Pass a function and watch it vanish
In the mini RSC, give
LikeButtona proponLike={() => console.log("liked")}insidePost. Predict the payload, then run. The function is missing from the JSON. A real RSC framework throws an error instead of dropping it silently (see Break it below). - 2
Move the state to the server
Add
useStatetoComments(a server component in the model). In the model it still runs because it's only a simulation, but in Next.js you'd get a build error saying the hook only works in a Client Component. Which file would you add"use client"to instead? - 3
Count the client code
List which functions in the mini RSC would be in the browser bundle in a real app (
LikeButton,Collapsible) and which never leave the server (Post,Comments,db,query).
Code & diagrams
A teaching model, not the real RSC format: `toPayload` plays the server (it awaits async components and leaves a `client` hole with props for each client component) and `fromPayload` plays the browser. Notice `Post` and `Comments` never appear in the payload, only their output, and `Comments` ran on the server even though the collapsible starts closed.
Preview
Console
Nothing logged yet.Expected console output
{"tag":"article","children":[{"tag":"h2","children":"Hello RSC"},{"client":"LikeButton","props":{"likes":3}},{"client":"Collapsible","props":{"title":"comments"},"children":{"tag":"ul","children":[{"tag":"li","children":"Nice!"},{"tag":"li","children":"Thanks"}]}}]}JSON silently drops the function and turns the `Date` into a string. React's real serializer keeps `Date` as a `Date`, but throws the error printed here (copied from React's server code) when a server component passes a function to a client component.
Preview
Console
Nothing logged yet.Expected console output
sent keys: [ 'name', 'price', 'tags', 'addedAt', 'formatPrice' ]
received keys: [ 'name', 'price', 'tags', 'addedAt' ]
addedAt: object -> string
Error: 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.// app/ui/collapsible.tsx
"use client";
import { useState } from "react";
export default function Collapsible({ title, children }: { title: string; children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<section>
<button onClick={() => setOpen(!open)}>{open ? "Hide" : "Show"} {title}</button>
{open && children}
</section>
);
}
// app/posts/[id]/page.tsx (server component)
import Collapsible from "@/app/ui/collapsible";
import Comments from "./comments"; // async server component
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
return (
<Collapsible title="comments">
<Comments postId={id} />
</Collapsible>
);
}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Render an async component in the browser
Take the async Comments component and render it on the client: createRoot(root).render(<Comments />). In Next.js the same happens when you add "use client" to a file with an async component.
Break #2
Pass an event handler from a server component
In a Next.js server component, write <LikeButton onLike={() => saveLike(id)} /> where LikeButton is a client component.
Myth vs fact
Myth
Server Components are the same as server-side rendering.
Fact
SSR turns components (including client ones) into HTML for the first load. RSC is a component type that runs only on the server and produces a payload, used on first load and on every later navigation. They work together but are different things; RSC can even run at build time with no server at request time.
Myth
"use client" means the component only runs in the browser.
Fact
Client components are still pre-rendered to HTML on the server for the first load, then hydrated. "use client" means "this code is also shipped to and runs in the browser".
Myth
You should add "use client" to every interactive component.
Fact
Only at the boundary. Everything imported from a client file is already client code. Extra directives are harmless but add noise.
Myth
Server Components replace client state.
Fact
They replace client-side data fetching for many cases. Interactive state (inputs, menus, optimistic UI) still lives in client components.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Directives (
"use client","use server") are read by the bundler, not by React at runtime. The framework's bundler builds two module graphs and swaps each client module, as seen from the server, for a reference{ id, name, chunks }. That's why RSC needs framework/bundler support and isn't something you add to a Vite SPA by importing a package. - ▸
Server components can pass a promise to a client component without awaiting it:
<Comments commentsPromise={getComments()} />, then the client callsuse(commentsPromise)inside<Suspense>. The server starts the work early and streams the resolved value later: render-as-you-fetch across the network. - ▸
Context doesn't work in server components (there's no
useContext). Put providers in a client component that takeschildrenand render it as deep as possible, so static parts of the tree above it can stay static. - ▸
cachefromreactdeduplicates a function per server request (the samegetUser(id)called by three components runs once). It is meant for server components: in the browser build of Reactcache(fn)still returns a function, but every call runsfnagain (nothing is memoized).
Remember this
- 1
A React Server Component (RSC) is a component that runs only on the server, either at build time or for each request. Because it never runs in the browser it can do things a browser can't: read a database or the file system, use secret API keys, and import big libraries (a Markdown parser, a date library) without sending them to the user. It can also be an
asyncfunction and simplyawaitits data:async function Post() { const post = await db.post.find(1); return <h1>{post.title}</h1>; }. Its code is never in the JavaScript bundle; only its rendered output is. - 2
A Client Component is the kind of component you've written in every earlier phase: it runs in the browser (and, in frameworks, is also pre-rendered to HTML on the server, Topic 20.1). Only client components can use state and effects (
useState,useEffect,useReducer...), event handlers (onClick), browser APIs (window,localStorage), context, and custom hooks that use these. Server components can't, because there is no user clicking on the server and the component doesn't stay alive after it renders. - 3
The **
"use client"directive, written as the very first line of a file, marks a boundary**: that file, and every module it imports, belongs to the client bundle. You don't put it on every client component; one directive at the entry point of an interactive part is enough. Files without it, in an RSC framework like Next.js App Router, are server components by default."use client"is about modules, not components: a server component can import and render a client component, but a client component can't import a server component (anything it imports becomes client code). - 4
Server components send their output as the RSC payload: a compact serialized description of the rendered tree, containing plain elements, the props for each client component, and references (a module id) to client components' code, which the browser loads and runs. So everything a server component passes to a client component as props must be serializable by React: strings, numbers, booleans,
null/undefined, BigInt, plain objects and arrays,Date,Map,Set, typed arrays, Promises (read them withuse()on the client), JSX elements, and Server Functions (Topic 20.3). Functions (like anonClickhandler), class instances and symbols not created withSymbol.forcannot cross. - 5
You can still put server-rendered UI inside a client component by passing it as **
childrenor another JSX prop**. In<Collapsible><Comments /></Collapsible>, the parent server component renders<Comments />on the server and passes the result into the clientCollapsible, which decides where to show it. This composition pattern keeps big data-heavy parts on the server even when they sit inside interactive wrappers like tabs, modals and accordions. - 6
Why bother? Bundle savings: a blog post page that formats Markdown and highlights code on the server ships zero bytes of those libraries. Data closer to the source: no client-side request waterfall (Topic 14.1), no public API needed just for your own pages. Security: secrets and queries stay on the server (but see Topic 20.3 and Phase 21 for what still leaks if you're careless). The costs: you need a framework that supports RSC (Next.js App Router, React Router's RSC support, and others), every interaction that needs new server data goes over the network, and you must think about which side each module lives on.
Explain it without notes
What is a React Server Component and what can it do that a client component can't?
What does "use client" do exactly? Where do you put it?
Which props can a server component pass to a client component, and why are functions not allowed?
How can a client component show content rendered by a server component?
What are the benefits and costs of Server Components?
Practice
A product page shows a description (Markdown converted with a 50 KB library), a price and an "Add to cart" button with a quantity picker. Decide which parts are server and which are client components, and write the client one.
Rewrite this to obey the boundary rules: a server component renders <SortableTable rows={rows} onSort={(col) => rows.sort(...)} />.
Trade-offs
- ↔
Moving a component to the server shrinks the bundle but makes every change to its data a server round trip; very interactive widgets (drag and drop, editors) belong on the client.
- ↔
Placing
"use client"high in the tree is easy but drags everything below it into the bundle; placing it low (leaf islands) keeps bundles small but needs more thought about props.
Done when you can
I can say what runs on the server and what runs in the browser in an RSC app.
I can place
"use client"at the right boundary.I know which props are serializable and why functions are not.
I can pass server-rendered UI into a client component through
children.I can use
server-onlyto protect server modules.I can explain the bundle and data benefits and the costs of Server Components.