Server Components and Client Components: the default and the escape hatch
Every component written so far — Home, PostsPage — has run on the server, even though nothing marked them that way. That's the App Router's default, and it's a genuinely different model from most React tooling that came before it. This part covers what it means in practice, and when to opt out of it.
Server Components run only on the server
// app/posts/page.tsx
export default function PostsPage() {
console.log("This runs on the server, not in the browser");
return <h1>All posts</h1>;
}That console.log shows up in the terminal running npm run dev, never in the browser's devtools console. A Server Component's code — including whatever it imports — never gets sent to the browser as JavaScript at all; the server runs it once, produces HTML, and that HTML is what ships. This is why Server Components can safely do things a browser never should, like reading a database directly or referencing a secret API key (part 16 covers this properly) — none of that code, or the key itself, is ever present in the client-side bundle.
Client Components: the explicit opt-in
// app/components/LikeButton.tsx
"use client";
import { useState } from "react";
export function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? "♥ Liked" : "♡ Like"}
</button>
);
}"use client" as the very first line of a file — before any import — is what marks everything in that file (and everything it exports) as a Client Component. useState, useEffect, and any onClick-style event handler only work inside a Client Component; a Server Component has no browser to run that code in at all, so React refuses to let it use hooks or handle DOM events directly.
Why the default matters
A traditional React app ships its entire component tree as JavaScript to the browser, then runs everything there. The App Router's default — server-first, client only where explicitly marked — means only the interactive pieces (LikeButton here) ship any JavaScript at all. A blog post's actual text content, rendered by a Server Component, adds nothing to the client-side bundle no matter how long the post is.
Mixing both on one page
// app/posts/page.tsx
import { LikeButton } from "@/components/LikeButton";
export default function PostsPage() {
return (
<div>
<h1>All posts</h1>
<LikeButton />
</div>
);
}PostsPage stays a Server Component — no "use client" at its top — while rendering LikeButton, which is one. This is the normal shape of a real page: server-rendered structure and content, with small, marked client "islands" for anything that genuinely needs interactivity.
Adding "use client" to a component "just in case," out of uncertainty about whether it needs it. Every Client Component adds to the JavaScript bundle sent to the browser — the default should stay a Server Component until something concrete (useState, an event handler, a browser-only API) actually requires the opt-out.
Next: layouts — shared UI (a header, a footer) that wraps every page under a route, written once instead of repeated in every page.tsx.