Next.js Server Components vs Client Components

Every component in Next.js runs somewhere — either on the server or in the browser. Understanding where your component runs is the single most important concept in the App Router. Get this right, and your app becomes faster, more secure, and easier to build.

The Core Idea

Think of a restaurant kitchen and a dining table. The kitchen (server) does all the heavy preparation work — chopping, cooking, seasoning. The dining table (client/browser) is where customers interact — they pick up forks, pour their own drinks, and choose what to eat first. You want as much work done in the kitchen as possible so the table experience is smooth and fast.

Server Component                    Client Component
────────────────                    ────────────────
Runs on the server                  Runs in the browser
No JavaScript sent to browser       JavaScript sent to browser
Can access databases directly       Cannot access databases
Cannot use useState/useEffect       Can use useState/useEffect
Cannot handle click events          Can handle click events
Faster initial load                 Enables interactivity

Server Components: The Default

In the App Router, every component is a Server Component by default. You do not need any special syntax to make a component run on the server — it just does.

// app/products/page.js
// This is a Server Component — no declaration needed

async function getProducts() {
  const res = await fetch("https://api.example.com/products");
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts();  // Direct data fetch — no useEffect!

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.name} — ${product.price}</li>
      ))}
    </ul>
  );
}

The browser receives finished HTML. No JavaScript for fetching data is sent to the user's device. The page loads fast even on slow connections.

What Server Components Can Do

Server Components have access to things that browser JavaScript cannot touch.

// Server Component powers:

// 1. Read files from the server
import fs from "fs";
const config = fs.readFileSync("./config.json");

// 2. Query a database directly
import db from "@/lib/database";
const users = await db.query("SELECT * FROM users");

// 3. Use secrets safely
const apiKey = process.env.SECRET_API_KEY;  // Never exposed to browser

// 4. Fetch data without waterfalls
const [user, posts, comments] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
  fetchComments(id),
]);

Client Components: Add Interactivity

When a component needs to respond to user actions, manage state, or run browser APIs, mark it as a Client Component using the "use client" directive at the very top of the file.

// components/Counter.js
"use client";  // ← This single line makes it a Client Component

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Add One</button>
    </div>
  );
}

The "use client" directive is like drawing a boundary line. Everything in this file and everything it imports runs in the browser.

What Client Components Can Do

// Client Component powers:

// 1. React hooks
const [value, setValue] = useState("");
const [data, setData] = useEffect();

// 2. Event handlers
<button onClick={handleClick}>Click me</button>
<input onChange={(e) => setValue(e.target.value)} />

// 3. Browser APIs
window.localStorage.setItem("theme", "dark");
navigator.geolocation.getCurrentPosition(callback);
document.querySelector(".modal").focus();

// 4. Real-time updates
useEffect(() => {
  const interval = setInterval(fetchLiveData, 5000);
  return () => clearInterval(interval);
}, []);

Diagram: The Component Boundary

Browser requests /dashboard
           │
           ▼
┌─────────────────────────────────────────┐
│              SERVER                     │
│                                         │
│  DashboardPage (Server Component)       │
│  ├── fetches user data from DB          │
│  ├── renders UserProfile (Server)       │
│  └── includes <LikeButton />           │
│                                         │
│  Next.js sends HTML + minimal JS ──────►│──► Browser
└─────────────────────────────────────────┘
                                          │
                          ┌───────────────▼──────────────┐
                          │           BROWSER            │
                          │                              │
                          │  HTML is already visible ✓  │
                          │  LikeButton hydrates         │
                          │  Click events now work ✓     │
                          └──────────────────────────────┘

The Golden Rule: Server Components Cannot Import Client Components That Use Hooks

Actually, that statement is slightly wrong — and the real rule matters. A Server Component can include a Client Component. But a Client Component cannot include a Server Component that uses server-only features.

// ✅ CORRECT: Server Component includes Client Component
// app/page.js (Server Component)
import LikeButton from "@/components/LikeButton"; // Client Component

export default async function Page() {
  const post = await fetchPost();
  return (
    <article>
      <h1>{post.title}</h1>
      <LikeButton postId={post.id} />  {/* Works fine */}
    </article>
  );
}

// ❌ WRONG: Cannot use async/await or db calls inside a Client Component
"use client";
export default async function BadComponent() {
  const data = await db.query("...");  // ERROR — cannot do this in Client Component
}

Passing Server Data to Client Components

Fetch data on the server, then pass it as props to the Client Component. The data travels as serialized JSON.

// app/page.js — Server Component fetches data
import LikeButton from "@/components/LikeButton";

export default async function Page() {
  const post = await fetchPost();  // Server-side fetch

  // Pass plain data as props to the Client Component
  return <LikeButton initialLikes={post.likes} postId={post.id} />;
}

// components/LikeButton.js — Client Component handles interaction
"use client";
import { useState } from "react";

export default function LikeButton({ initialLikes, postId }) {
  const [likes, setLikes] = useState(initialLikes);

  async function handleLike() {
    setLikes(likes + 1);
    await fetch(`/api/posts/${postId}/like`, { method: "POST" });
  }

  return <button onClick={handleLike}>❤️ {likes}</button>;
}

The Children Pattern: Keeping Server Components Inside Client Components

What if you need a Client Component to wrap some Server Component output? Use the children prop. The Server Component runs on the server and its output gets passed as children into the Client Component.

// components/Modal.js — Client Component (handles open/close state)
"use client";
import { useState } from "react";

export default function Modal({ children }) {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setOpen(true)}>Open</button>
      {open && <div>{children}</div>}
    </div>
  );
}

// app/page.js — Server Component composes the tree
import Modal from "@/components/Modal";
import UserProfile from "@/components/UserProfile"; // Server Component

export default async function Page() {
  return (
    <Modal>
      <UserProfile />   {/* This Server Component runs on the server! */}
    </Modal>
  );
}

When to Use Each Type

Use a Server Component when:

✓ Fetching data from an API, database, or file system
✓ Accessing environment variables or secrets
✓ Rendering content that does not need interactivity
✓ Displaying lists, articles, product pages, blog posts
✓ Reducing the JavaScript sent to the browser

Use a Client Component when:

✓ Using useState, useEffect, useRef, or other React hooks
✓ Handling button clicks, form inputs, keyboard events
✓ Using browser APIs (localStorage, window, document)
✓ Building interactive components: modals, dropdowns, sliders
✓ Using third-party libraries that need the browser environment

Performance Impact: A Real Comparison

Page: Product listing with 100 items

Without Server Components (old Pages Router):
  Server sends → HTML shell + large JS bundle
  Browser downloads JS → runs JS → fetches data → renders items
  Time to content: ~2–4 seconds on slow connection

With Server Components (App Router):
  Server fetches data → renders full HTML → sends to browser
  Browser receives complete HTML immediately
  Time to content: ~0.3–0.8 seconds
  JS bundle size: significantly smaller (no fetch logic in browser)

Checking Your Component Type

A quick mental checklist:

Does the component use...
  useState, useEffect, useRef?         → Client Component
  onClick, onChange, onSubmit?         → Client Component
  window, document, localStorage?     → Client Component
  Custom hooks that use the above?     → Client Component

Does the component...
  Fetch from a database or API?        → Server Component (preferred)
  Read environment variables?          → Server Component
  Only display data without clicks?    → Server Component
  Import heavy libraries once?         → Server Component (keeps them off browser)

Common Mistake: "use client" Spreads Too Far

Adding "use client" to a component makes that component AND all its imports run in the browser. If you put it on a top-level layout, you lose all server-rendering benefits for every component below it.

// ❌ BAD: Putting "use client" too high
"use client";
// app/layout.js — Now NOTHING in this app benefits from server rendering

// ✅ GOOD: Push "use client" as far down the tree as possible
// app/layout.js — Server Component (no directive needed)
// app/components/SearchBar.js — "use client" only on the interactive part

Keep the "use client" boundary as close to the leaf components as possible. The deeper you push it, the more of your app runs efficiently on the server.

Leave a Comment