REACT & NEXT.JS / 6. SERVER VS CLIENT COMPONENTS

Server vs Client Components

The most important mental model in Next.js App Router


EXPLANATION

This is the biggest shift in Next.js 13+. Every component is a Server Component by default.

Server Components:
• Run only on the server, never sent to the browser as JS
• Can directly access databases, file system, environment variables
• Cannot use useState, useEffect, event handlers, browser APIs
• Reduce client-side JavaScript bundle significantly

Client Components:
• Add "use client" directive at top of file
• Run on both server (for initial HTML) and client (for interactivity)
• Can use hooks, event handlers, browser APIs
• Needed for: useState, useEffect, onClick, window, localStorage

The composition pattern:
• Server Component fetches data, passes it as props to a Client Component
• "Push state down, push data up" — keep interactivity at the leaf level

Common mistake: marking an entire page as "use client" just because one small part needs interactivity. Instead, extract just the interactive part into its own Client Component and keep the parent as a Server Component.

DIAGRAM

✓ CORRECT: server wraps client
  ServerPage (server)          ← fetches data, no JS bundle
    └── <StaticContent />      ← server component
    └── <InteractiveButton />  ← "use client" (small JS chunk)

  ✗ WRONG: unnecessary "use client" on parent
  Page ("use client")          ← entire tree becomes client JS!
    └── <StaticContent />      ← now also client (unnecessary)
    └── <InteractiveButton />  ← client

  Server Components CAN:        Client Components CAN:
  ✓ async/await                 ✓ useState, useEffect
  ✓ fetch() with caching        ✓ onClick, onChange
  ✓ access DB directly          ✓ browser APIs
  ✓ read env variables          ✓ custom hooks
  ✗ hooks                       ✗ async at component level
  ✗ event handlers              ✗ direct DB access

CODE

TSX
1// ── Server Component fetches data directly ─────────────────────
2// app/dashboard/page.tsx (NO "use client" default is server)
3import { db } from "@/lib/db";
4import { StatsCard } from "./StatsCard"; // client component
5
6export default async function DashboardPage() {
7 // Direct DB access only possible in server components
8 const stats = await db.query("SELECT COUNT(*) FROM users");
9
10 // Read env variables safe, never exposed to client
11 const apiVersion = process.env.API_VERSION;
12
13 return (
14 <div>
15 <h1>Dashboard</h1>
16 {/* Pass server data to client component as props */}
17 <StatsCard initialCount={stats.count} />
18 </div>
19 );
20}
21
22// ── Client Component handles interactivity ──────────────────────
23// app/dashboard/StatsCard.tsx
24"use client";
25
26import { useState } from "react";
27
28export function StatsCard({ initialCount }: { initialCount: number }) {
29 const [count, setCount] = useState(initialCount); // hooks work here
30
31 return (
32 <div onClick={() => setCount(c => c + 1)}> // events work here
33 Count: {count}
34 </div>
35 );
36}
37
38// ── Pattern: server fetches, client displays ──────────────────────
39// Server Component
40async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
41 const { id } = await params;
42 const product = await fetch(`https://api.example.com/products/${id}`).then(r => r.json());
43
44 return <ProductDetails product={product} />; // ProductDetails is client
45}
46
47// Client Component
48"use client";
49function ProductDetails({ product }: { product: Product }) {
50 const [qty, setQty] = useState(1);
51
52 return (
53 <div>
54 <h1>{product.name}</h1>
55 <p>${product.price}</p>
56 <input type="number" value={qty} onChange={e => setQty(+e.target.value)} />
57 <button onClick={() => addToCart(product.id, qty)}>Add to Cart</button>
58 </div>
59 );
60}
61
62// ── Server Component: reading cookies and headers ─────────────────
63import { cookies, headers } from "next/headers";
64
65async function ProtectedPage() {
66 const cookieStore = await cookies();
67 const token = cookieStore.get("auth-token");
68
69 if (!token) redirect("/login");
70
71 const user = await validateToken(token.value);
72 return <div>Welcome, {user.name}</div>;
73}
74
75type Product = { id: string; name: string; price: number };
76function addToCart(id: string, qty: number) {}
77async function validateToken(token: string) { return { name: "Kamran" }; }
78function redirect(path: string) {}
← PREV5. App Router & LayoutsNEXT →7. Data Fetching & Caching