Chapter 04 — Server Components vs Client Components (The Big Mental Shift)

Chapter 04 — Server Components vs Client Components (The Big Mental Shift)

Hey everyone! This is, without question, the most important chapter in this series. If you understand this one deeply, everything else in Next.js — data fetching, caching, performance — will make sense. If you skim it, you'll spend weeks confused about "why is this hook not working" or "why can't I use useState here."

What we will cover:

  • The default you didn't choose: everything is a Server Component
  • What Server Components CAN and CANNOT do
  • The "use client" directive — what it really means
  • What Client Components CAN and CANNOT do
  • How to mix them (the correct pattern)
  • A simple decision rule for every component you write

1. The Default You Didn't Choose

In plain React, every component runs in the browser. Simple, one world, no decision to make.

In the Next.js App Router, every component is a Server Component by default — unless you explicitly opt out. This is a huge, silent shift, and it's the #1 source of confusion for React developers moving to Next.js.

// app/page.js — this is a SERVER COMPONENT, even though it looks like normal React
export default function HomePage() {
    return <h1>Hello</h1>;
}

Nothing marks it as special. No import, no directive.
Server Component is simply THE DEFAULT in the app/ folder.

2. What "Server Component" Actually Means

SERVER COMPONENT:
==================
  Runs ONLY on the server. Never shipped to the browser as JS.
  Renders to a special format (RSC payload), which becomes HTML.
  The browser receives HTML + a tiny bit of JS for interactivity
  elsewhere on the page — NOT this component's code.

  Result: this component adds ZERO kilobytes to your JS bundle.

This is the headline benefit: Server Components ship no JavaScript to the browser. A component that just displays data doesn't need to exist client-side at all — so Next.js simply doesn't send it.

3. What Server Components CAN Do

  • ✅ Fetch data directly — await fetch(...) right in the component, no useEffect
  • ✅ Access backend resources directly — databases, file system, environment secrets
  • ✅ Keep large dependencies (a markdown parser, a date library) OUT of the client bundle
  • ✅ Render other Server Components AND Client Components
// Server Component — fetching data directly, no hooks needed
async function ProductList() {
    const products = await db.product.findMany();  // straight to the database!

    return (
        <ul>
            {products.map(p => <li key={p.id}>{p.name}</li>)}
        </ul>
    );
}

4. What Server Components CANNOT Do

❌ useState, useEffect, useReducer   → no state, no lifecycle, no browser APIs
❌ onClick, onChange, any event handler → nothing to "handle", there's no browser JS running it
❌ useContext                         → context needs a live component tree in the browser
❌ window, localStorage, document     → these don't exist on the server

Why? Because this component's code literally never reaches the browser. There's no live JS instance sitting there to hold state or respond to a click. It rendered once, on the server, into HTML — and that's the end of its job.

5. The "use client" Directive

When you DO need interactivity — a button, a form, a counter — you opt in to the browser with one line at the top of the file:

"use client";

import { useState } from "react";

export default function Counter() {
    const [count, setCount] = useState(0);
    return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Important misconception to avoid: "use client" does NOT mean "this component only renders on the client." It still gets rendered once on the server for the initial HTML (fast first paint!) — but then it also ships its JS to the browser and becomes interactive there. Think of it as "this component NEEDS the browser," not "this component IS the browser."

6. What Client Components CAN & CANNOT Do

Server ComponentClient Component
useState / useEffect
onClick / onChange
Direct DB / filesystem access
async/await in the component body❌ (use useEffect instead)
Ships JS to the browser❌ (0kb)
Can import a Server Component❌ (see below)

7. The Correct Pattern: Mixing Them

The rule people get backwards: a Server Component CAN render a Client Component, but a Client Component CANNOT import a Server Component directly.

✅ ALLOWED — Server renders Client:
=====================================
// app/page.js (Server Component)
import LikeButton from "./LikeButton";  // a Client Component

export default async function ProductPage() {
    const product = await getProduct();     // server-only data fetch
    return (
        <div>
            <h1>{product.name}</h1>         {/* stays server-rendered, 0kb JS */}
            <LikeButton productId={product.id} />  {/* becomes interactive */}
        </div>
    );
}

❌ NOT ALLOWED — Client importing Server directly:
=====================================================
"use client";
import ProductDetails from "./ProductDetails"; // a Server Component — breaks!

// FIX: pass it as "children" instead — from a Server Component parent
The "children" workaround, when you truly need this shape:
==============================================================
// Server Component (parent)
<ClientWrapper>
    <ServerChild />      {/* passed in from the server-rendered parent */}
</ClientWrapper>

// Client Component
"use client";
export default function ClientWrapper({ children }) {
    const [open, setOpen] = useState(false);
    return <div onClick={() => setOpen(!open)}>{open && children}</div>;
}

8. The Decision Rule

For every component you write, ask:
=======================================
  "Does this need state, effects, event handlers, or browser-only APIs?"

    NO  → leave it as a Server Component (the default). Do nothing.
    YES → add "use client" at the top.

  Practical tip: push "use client" as FAR DOWN the tree as possible.
  Don't mark your whole page client-side just because one button
  needs an onClick — wrap only that button.

Key Points to Remember

  • Server Components are the default in the App Router — no import, no directive needed
  • Server Components ship zero JS to the browser and can fetch data / touch the DB directly
  • "use client" opts a component INTO the browser — hooks, events, browser APIs become available
  • Client Components still render once on the server for fast first paint, then hydrate in the browser
  • Server → Client imports are fine; Client → Server imports are not (pass Server Components as children instead)
  • Keep "use client" boundaries as small/deep as possible for the smallest JS bundle

What's Next?

Now that you know WHERE code runs, next chapter is about data — fetching it, and understanding Next.js's (surprisingly aggressive) caching defaults.

Keep coding, keep learning! See you in the next one!