Server Components
A component that never reaches the browser
Section titled “A component that never reaches the browser”A React Server Component (RSC) runs on the server, produces its output there, and never ships its code to the client. The browser receives the result, not the component. That has two big payoffs: the component’s dependencies (a Markdown parser, a database client) stay off the client bundle, and the component can access server resources directly.
Because it runs on the server, a Server Component can be async and fetch data inline — no useEffect, no loading state, no client round-trip:
// Server Component — runs on the server. Note: it's an async function.export default async function ProductPage({ id }) { const product = await db.products.findById(id); // direct DB access, on the server return ( <article> <h1>{product.name}</h1> <p>{product.description}</p> </article> );}There is no 'use client' here, so this is a Server Component. It awaits data and returns JSX; the client only ever sees the rendered markup.
The server/client boundary: 'use client'
Section titled “The server/client boundary: 'use client'”Server Components can’t do interactive things — no useState, no onClick, no browser APIs, because that code never runs in the browser. When you need interactivity, you cross into a Client Component by putting the 'use client' directive at the top of the file:
'use client';import { useState } from 'react';
// Client Component — ships to the browser, can use state and events.export default function AddToCart({ productId }) { const [count, setCount] = useState(0); return <button onClick={() => setCount((c) => c + 1)}>In cart: {count}</button>;}'use client' marks the boundary: that file and everything it imports become client code. Server Components can render Client Components (a server page can include an interactive button), but not the reverse in the same way — the direction of the boundary matters.
flowchart TB
subgraph server["Server (never shipped)"]
sc["Server Component
async, DB access, secrets"]
end
subgraph client["Client (shipped to browser)"]
cc["Client Component
use client, useState, onClick"]
end
sc -->|renders + passes
serializable props| cc What can cross the boundary
Section titled “What can cross the boundary”A Server Component passes props to a Client Component, but those props must be serializable — they have to survive being sent over the network as data:
- ✅ Strings, numbers, booleans, plain objects/arrays, JSX, and promises (the client resolves them with
use()). - ❌ Functions (event handlers), class instances, or anything holding live state — these can’t be serialized and sent.
// Server Component passes a promise across the boundary:export default function Page() { const commentsPromise = fetchComments(); // not awaited here return <Comments commentsPromise={commentsPromise} />; // Client resolves it with use()}RSC comes from a framework
Section titled “RSC comes from a framework”One honest caveat: you don’t get Server Components from bare react + a bundler. RSC needs a framework or build setup that understands the server/client split and can serialize the boundary — in practice, Next.js (App Router), and other RSC-capable frameworks. The concepts here are React’s; the wiring is the framework’s.
Server Functions: 'use server'
Section titled “Server Functions: 'use server'”So far, data has only flowed one direction — server to client. The other half of the story is calling back into the server: a Server Function is an async function marked with 'use server' that the client can call directly, as if it were a local function, even though it actually triggers a network request. Per react.dev:
Add
'use server'at the top of an async function body to mark the function as callable by the client. We call these functions Server Functions. — react.dev/reference/rsc/use-server
You can mark a single function, or put 'use server' at the top of a whole file to make every export in it a Server Function:
import { db } from './lib/db';
export async function addToCart(productId: string, quantity: number) { 'use server'; await db.cart.add({ productId, quantity });}Import it into a Client Component and hand it to useActionState exactly like the plain client-side actions from the next lesson — the only difference is where the function body actually runs:
'use client';
import { useActionState } from 'react';import { addToCart } from './actions';
type State = string | null;
// addToCart is a Server Function: calling it here serializes (productId,// quantity) and sends them to the server, where the real function body runs.async function submit(_prevState: State, formData: FormData): Promise<State> { const productId = String(formData.get('productId')); const quantity = Number(formData.get('quantity')); await addToCart(productId, quantity); return null;}
export function AddToCartButton({ productId }: { productId: string }) { const [error, formAction, isPending] = useActionState(submit, null); return ( <form action={formAction}> <input type="hidden" name="productId" value={productId} /> <input type="number" name="quantity" defaultValue={1} min={1} /> <button type="submit" disabled={isPending}>Add to cart</button> {error && <p>{error}</p>} </form> );}export const db = { cart: { add: async (_args: { productId: string; quantity: number }) => {}, },};Both fences above type-check with plain tsc — the shapes are ordinary TypeScript. What they can’t do is actually run: 'use server' only means something to an RSC-capable bundler that rewrites the call into a network request. This course’s Vite probe has no such bundler, so there is nothing this page can honestly show you executing — only that the code is well-typed. For the real, running wiring (the framework half of this story), see the Next.js Deep Dive course’s What are Server Actions?.
The arguments and return value of a Server Function cross the network, so the same serialization rule from the previous section applies to them too: primitives, plain objects/arrays, and promises travel; functions and class instances don’t.
RSC payload and Flight, briefly
Section titled “RSC payload and Flight, briefly”When a Server Component renders, React doesn’t send HTML strings to the client — it sends a serialized description of the element tree (informally called the “RSC payload,” produced by a wire format nicknamed “Flight”). The client’s React reconstructs Client Components from that description and slots in the Server Components’ already-rendered output. This is what makes use() on a promise work across the boundary, and it’s also why the boundary has serialization rules in the first place — the payload is data, not code. The full request lifecycle (routing, this payload, streaming, hydration) is covered end to end in the Next.js Deep Dive course’s request lifecycle lesson; this course only needs you to know the payload exists and why it constrains what can cross the boundary.
Mental Model
Section titled “Mental Model”Three things move across the server/client line, and each has its own rule: rendered output flows server → client as the RSC payload (always allowed, that’s the whole point); props flow server → client and must be serializable; calls flow client → server only through a Server Function, whose arguments and return value are serialized the same way. Nothing here is really about “where code runs” in the abstract — it’s about what has to survive being turned into data and sent over a wire.
Trade-offs
Section titled “Trade-offs”| Choice | What you get | Cost |
|---|---|---|
| Server Component | Direct server-resource access, zero client bundle cost, can be async | No state, no events, no browser APIs — ever |
Client Component ('use client') | Full interactivity | Ships to the browser; its whole module tree becomes client bundle |
Server Function ('use server') | Client can trigger server-side work without a hand-written API route | Only serializable arguments/return values; requires an RSC-capable framework to actually run |
Common Misconceptions
Section titled “Common Misconceptions”- “
'use client'means this code only runs on the client.” It means the opposite direction of what people expect: it marks a module as renderable from the client, so its exports become client references a Server Component can render. The file still gets executed wherever it’s rendered, including being pre-rendered on the server for the initial HTML. - “Any function can be a Server Function if you import it into a Client Component.” Only a function whose body (or whose file) starts with
'use server'becomes a callable server reference. Importing an ordinary function into a Client Component just runs it on the client, like any other import. - “RSC is a React feature you can use in any React app.” The concepts (Server/Client Components,
'use server') are part of React, but only a framework that implements the RSC protocol — serializing the payload, wiring the network call for Server Functions — can actually run them. Plainreact+ Vite cannot.
Reading AI-generated code
Section titled “Reading AI-generated code”When you see 'use server' or 'use client' in a generated snippet, check:
- Is
'use client'actually present in every file that callsuseState,useEffect, or attaches an event handler? Its absence doesn’t error in this Vite probe (the directive is just a string to plaintsc), but a real RSC framework will reject the file the moment it tries to use a client-only API without it. - Is a Server Function invoked from an event handler or a form
action— or is it called directly during render? Calling it during render is a side effect in the render body, which breaks the same purity rule as any other side effect, and it also means the “request” fires on every render, not on user intent. - Are the arguments passed to a Server Function serializable? A generated snippet that passes a callback, a
Map, or a class instance as an argument will fail once it actually crosses the network boundary in a real framework, even though nothing in this course’s Vite probe can catch that for you. - Does a Client Component receive a function prop from a Server Component (not from another Client Component)? That specific direction is the one that can’t be serialized.
🐛 Spot the bug
'use client'
import { addToCart } from './actions'
export function AddToCartButton({ productId }: { productId: string }) {addToCart(productId, 1)return <button>Add to cart</button>}Show the answer
addToCart is called directly in the component body, not from an event handler or a form action. That means it fires on every render of AddToCartButton — including re-renders caused by unrelated state changes higher in the tree — instead of once when the user clicks. This is the same “side effect during render” problem Foundations covers for useState/mutation, just with a network call standing in for the side effect. The fix is to only call it from onClick, or better, wire it as the <form action={...}> the way useActionState does earlier in this lesson.