Server vs. Client Components
The mental model behind React Server Components — why every component is server-only until you say otherwise.
3 phút đọc
This is the single biggest conceptual shift from a plain React app: in the App Router, every component is a Server Component by default. It doesn't run in the browser at all — it runs on the server (or at build time), produces output, and the browser only ever sees the result. Nothing about the component's own code — no imports, no logic — is sent to the client unless you explicitly opt in.
What Server Components can do that Client Components can't
Because a Server Component's code never leaves the server, it can safely do things you'd never want exposed to a browser tab:
// app/dashboard/page.tsx — a Server Component (no directive needed)
import { db } from "@/lib/db";
export default async function DashboardPage() {
// Runs on the server. The database connection string, the query itself,
// and any secrets involved never reach the browser.
const users = await db.query("SELECT id, name FROM users");
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}Server Components can be async functions directly — no useEffect, no loading state management for the initial fetch. They can also read files, call an ORM, or use an API key from an environment variable, none of which is safe to do from code that ships to a browser.
What Server Components can't do
The trade-off is that Server Components have no access to anything that only exists in a browser tab: no useState, no useEffect, no event handlers like onClick, no window or localStorage. They render once on the server and produce static output for that request — there's no "component instance" living in the browser that could hold state or respond to a click.
// This does NOT work in a Server Component — no interactivity, no hooks
export default function Counter() {
const [count, setCount] = useState(0); // Error: useState is client-only
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}Client Components: opting in
For anything interactive, you mark a file as a Client Component with the "use client" directive at the very top, before any imports:
// app/ui/counter.tsx
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count} likes</button>;
}A Client Component still renders once on the server for the initial HTML (so there's no blank flash), then hydrates in the browser — React attaches event listeners to that existing markup so it becomes interactive, without throwing away and re-rendering everything from scratch.
Why this distinction exists at all
The payoff isn't just architectural tidiness — it's how much JavaScript actually ships to visitors. A page built entirely from Server Components sends effectively zero component JavaScript to the browser; only the rendered HTML. Every "use client" boundary you add pulls that file, and everything it imports, into the browser bundle. Server Components are how Next.js keeps initial page loads fast even as an app grows — you pay the JavaScript cost only where you actually need interactivity, not for the whole page by default.