Server Components Explained
use client, use server, the network boundary in JSX.
For a decade, React lived entirely in the browser. Then 2023 happened, and the framework grew a second home: the server. React Server Components (RSC) are not server-side rendering. They're not getServerSideProps. They're a new kind of component that only runs on the server, ships zero JavaScript to the client, and composes seamlessly with the interactive components you already know. In Next.js App Router, Server Components are the default. Once that sinks in, the whole architecture clicks.
Two kinds of component, one tree
Every component in an App Router app is either a Server Component or a Client Component. The rules are simple:
- Server Components are the default. They run on the server (during request or at build time), can be
async, can read files, query databases, hit private APIs. They don't ship JavaScript to the browser. - Client Components opt in with the directive
"use client"at the top of the file. They run in the browser (and during SSR), can use hooks, state, effects, and event handlers.
// no directive needed; Server is the default
import { db } from "@/server/db";
import { AddToCartButton } from "./add-to-cart";
export default async function ProductsPage() {
const products = await db.product.findMany();
return (
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name}
<AddToCartButton id={p.id} />
</li>
))}
</ul>
);
}"use client";
import { useState } from "react";
export function AddToCartButton({ id }: { id: string }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)}>
{added ? "Added!" : "Add to cart"}
</button>
);
}"use client" marks an entry pointinto the client. Every component this file imports (and they import) becomes a client component too. So you only put the directive on the top of a tree, not everywhere.What can run where?
The boundary is sharp. Knowing what each side can do prevents most RSC bugs.
Server Components can
- Be
asyncand useawaitdirectly in the body - Read files, env secrets, query databases
- Import server-only libraries (Node SDKs, ORMs)
- Render Client Components as children
Server Components cannot
- Use
useState,useEffect, or any hook - Add event handlers like
onClick - Use browser APIs (
window,localStorage) - Subscribe to context that's only set up on the client
Client Components can
- Use all React hooks
- Attach event handlers, read user input
- Access
window(after mount) - Render Server Components only as children, never by importing them
The serializable props rule
Server Components run on the server and produce a payload that gets shipped to the browser. Any prop that crosses from server to client has to fit in that payload, which means it must be serializable. JSON-compatible values work: strings, numbers, booleans, arrays, plain objects, null. Functions do not.
// ✓ Fine
<ClientThing user={{ name: "Ada", age: 36 }} />
// ✗ Will throw at runtime
<ClientThing onSubmit={(data) => savePost(data)} />"use server" can be passed across the boundary. Under the hood Next replaces them with a serialized reference that calls back to the server. More on those in the next lesson.Composition: server inside client
Client Components can't import Server Components, but they can render them as children. This is how you get a fancy interactive shell with server-rendered guts.
import { Sidebar } from "./sidebar"; // client
import { Feed } from "./feed"; // server
export default function Layout() {
// Sidebar is a client component, but Feed (server) is passed
// as children; the server still renders Feed.
return (
<Sidebar>
<Feed />
</Sidebar>
);
}"use client";
import { useState } from "react";
export function Sidebar({ children }: { children: React.ReactNode }) {
const [collapsed, setCollapsed] = useState(false);
return (
<div>
<button onClick={() => setCollapsed((c) => !c)}>Toggle</button>
{!collapsed && children}
</div>
);
}Read that twice. Sidebar is interactive, butFeedstill does its database query on the server. The two coexist in one tree. The pattern of "pass server JSX as children" is the most common RSC pattern in real apps.
Mental model: what arrives in the browser
Open DevTools. Look at what actually gets sent. For a typical RSC page you'll see two things:
- HTML: the fully rendered page, like traditional SSR, so the first paint is fast and search engines see it.
- RSC payload: a streaming text format that describes the React tree, including references to the Client Components that need to hydrate. JavaScript bundles get loaded only for those Client Components.
Server-only components literally never become JavaScript bundles. A 50KB markdown parser used inside a Server Component adds zero bytes to your client JS. That's the perf story.
When to choose which
- Need
useStateoronClick→ Client. - Need to await a database or read a secret → Server.
- Both? Split the component. Keep the server parent as default and extract the interactive bit into a smaller client child. This is the pattern.
"use client"at the top of every file because that's how React used to work. Resist. Smaller client islands ship less JavaScript and stay faster. Only opt in where you genuinely need interactivity.Quiz
Which is the default in the Next.js App Router?
Recap
- App Router defaults to Server Components; opt into client with
"use client"at the top of the file. - Server Components ship zero JS, can be async, and can read servers/databases directly.
- Client Components own state, effects, and event handlers.
- Props across the boundary must be serializable; functions don't cross (except Server Actions).
- Compose by passing server JSX as
childreninto client components.