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 interactivityServer 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 browserUse 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 environmentPerformance 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 partKeep 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.
