Server Components vs Client Components: A Mental Model for the App Router

The most common App Router mistake isn't a bug — it's reflexively adding "use client" to the top of every file out of habit from the Pages Router days. The mental model that actually helps: Server Components are the default, and the network boundary is not the component tree — it's wherever you draw a "use client" line.
What actually runs where
Server Components render on the server (or at build time), never ship their JS to the browser, and can talk directly to a database or the filesystem — no API route needed.
Client Components render on the server for the initial HTML too, then hydrate in the browser and re-render there. Their JS does ship to the client.
Everything is a Server Component by default in the App Router. "use client" at the top of a file marks that file and everything it imports as part of the client bundle.
The rule of thumb
Ask: does this component need state, effects, event handlers, or a browser-only API? If no, leave it a Server Component.
// This is a Server Component (no directive needed) — it can await a
// database call directly, and none of this code ships to the browser.
import { prisma } from "@/lib/prisma";
export default async function ProjectList() {
const projects = await prisma.project.findMany();
return (
<ul>
{projects.map((p) => <li key={p.id}>{p.titleEn}</li>)}
</ul>
);
}"use client";
// This one needs state and an event handler, so it has to be a Client
// Component — but notice it's small and focused, not the whole page.
import { useState } from "react";
export default function LikeButton({ initialCount }: { initialCount: number }) {
const [count, setCount] = useState(initialCount);
return <button onClick={() => setCount((c) => c + 1)}>❤ {count}</button>;
}The composition trick everyone misses
"use client" doesn't mean the whole subtree becomes client-rendered — a Client Component can still render Server Components passed to it as children/props. This is the pattern that keeps most of your page server-rendered even when you need one interactive island.
"use client";
export function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return open ? <div className="modal">{children}</div> : null;
}
// In a Server Component:
import { Modal } from "./modal";
import ServerRenderedComments from "./comments"; // stays a Server Component
export default function Page() {
return (
<Modal>
<ServerRenderedComments /> {/* rendered on the server, passed as a child */}
</Modal>
);
}Common mistakes
Marking a layout or page "use client" just because one button inside it needs an onClick — extract the button instead.
Fetching data in a Client Component with useEffect when a Server Component could have just awaited it — slower (waterfall after hydration) and more code.
Passing non-serializable props (functions, class instances) from a Server to a Client Component — only serializable data crosses that boundary.
Forgetting that anything imported by a "use client" file is also bundled for the client, even if that specific import doesn't use any client APIs.
Push "use client" as far down the tree as it will go. The goal isn't zero client components — it's the smallest possible client components, surrounded by server-rendered everything else.
More posts

Authentication with better-auth in Next.js: A First-Hand Setup
The exact setup behind this site's own admin login: server config, the Next.js catch-all route handler, the client, and the two-layer middleware + layout protection that actually secures /admin.

Recharts in Next.js + Tailwind: Theming and Custom Styling
Making Recharts feel native to a Tailwind design system: correct Client Component boundaries, wiring colors to CSS variables for automatic dark mode, custom tooltips/legends, and handling RTL locales.