Server by default
Every component in the App Router is a server component until something says otherwise. That is the opposite of what React has meant for ten years, and it is the single idea this lesson is about.
| Server component | Client component | |
|---|---|---|
| Runs | On the server, at request or build time | On the server once (HTML), then in the browser |
| Ships JavaScript | None | Yes — it is in the bundle |
May be async | Yes | No |
Database, secrets, fs | Yes | Never |
useState, useEffect | No | Yes |
onClick, onChange | No | Yes |
window, localStorage | No | Yes |
The boundary
- The directive marks an entry point, not one component: every module imported from that file is client code too.
- One
"use client"near a leaf costs a few kilobytes. One at the root of the tree ships the whole application. - A client component cannot import a server component — but it can receive one as
children.
"use client";
import { useState } from "react";
export function Quantity({ initial }) {
const [n, setN] = useState(initial);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}
Passing a server component through
children is created where it is written — on the server — and handed to the client component as an already-rendered tree. The interactivity is in the wrapper; the data and the markup stay on the server.
// app/page.tsx — a server component
export default async function Page() {
const doors = await db.doors.all(); // server only
return (
<Accordion> {/* client: it has the open/closed state */}
<DoorList doors={doors} /> {/* still a SERVER component */}
</Accordion>
);
}
What can cross
<Client
name="Painted" // yes: strings, numbers, booleans, null, undefined
door={{ id: 1 }} // yes: plain objects and arrays, any depth
when={new Date()} // yes: Date, Map, Set, BigInt, promises
onSave={saveDoor} // only if saveDoor is a "use server" action
format={(d) => d} // NO: an ordinary function
row={new DoorRow()} // NO: a class instance — the methods do not survive
/>
Prisma and friends often return objects that look plain and are not. Map them to the fields you need before passing them down — which you should be doing anyway, so a password hash does not travel to the browser.
Keep the client part at the leaves
The question to ask about any component is: does it need state, an effect, an event handler or a browser API? If not, it has no reason to be in the bundle at all.
// bad: the whole page is client code because one button needs state
"use client";
export default function Page() { … <button onClick={…}> … }
// good: the page stays on the server, one small component is client
export default async function Page() {
const doors = await getDoors();
return (<><DoorTable doors={doors} /><AddButton /></>); // AddButton: "use client"
}
Secrets, and the direction of trust
Server components make it easy to query the database directly from a component. That is genuinely convenient, and it means an accidental "use client" at the top of that file is a build error rather than a leak — which is the safety the boundary buys.
// server component — fine
const key = process.env.STRIPE_SECRET_KEY;
// client component — undefined, unless the name starts with NEXT_PUBLIC_,
// and anything NEXT_PUBLIC_ is visible to every visitor. It is not a secret.
Hydration, and the errors it produces
// "Text content did not match" — three classic causes
<p>{new Date().toLocaleString()}</p> // different clock
<p>{Math.random()}</p> // different value
<p>{localStorage.getItem("theme")}</p> // not available on the server
// render it after mount instead
const [ready, setReady] = useState(false);
useEffect(() => setReady(true), []);
return <p>{ready ? new Date().toLocaleString() : null}</p>;