When to Use Client Components
When to Use Client Components
The most common mistake Next.js developers make is adding "use client" everywhere — treating the App Router like a regular React app. The other extreme is trying to make everything a Server Component and running into walls. This lesson gives you the decision framework to use each correctly.
The Default Is Server
Without "use client", a component is a Server Component. Server Components:
- Run on the server (at request time or build time)
- Can be
asyncandawaitdata directly - Have no client-side JavaScript bundle impact
- Can access Node.js APIs, environment variables, the filesystem
- Cannot use React hooks, event handlers, or browser APIs
Client Components ("use client"):
- Run in the browser (and also on the server for initial HTML — this is important)
- Can use
useState,useEffect,useRef, and all React hooks - Can attach event listeners
- Access
window,document,localStorage - Are bundled and sent to the browser
The Decision Tree
Does this component need:
│
├── onClick, onChange, onSubmit, or any user interaction?
│ └── YES → "use client"
│
├── useState, useEffect, useRef, useReducer, or any hook?
│ └── YES → "use client"
│
├── window, document, localStorage, or any browser API?
│ └── YES → "use client"
│
├── A third-party library that uses browser APIs?
│ (charting libraries, rich text editors, date pickers)
│ └── YES → "use client"
│
├── Database query, server-side API call, or secret env var?
│ └── Server Component (no "use client")
│
├── Static markup that never changes based on user interaction?
│ └── Server Component
│
└── A component that just receives props and returns JSX?
└── Could be either — start Server, add "use client" if needed
Practical Examples
Always Server
// Static blog post — no interactivity needed
// src/app/blog/[slug]/page.tsx
import { getPost } from "@/lib/posts";
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug); // database query
return <article>{post.content}</article>;
}
// Navigation that's purely rendered server-side
// (if it has no interactive dropdown, active state tracking, etc.)
async function Nav() {
const categories = await getCategories();
return (
<nav>
{categories.map(c => (
<a key={c.id} href={`/courses?category=${c.slug}`}>{c.name}</a>
))}
</nav>
);
}
// Price display — just formats and shows a number
function PriceTag({ price, currency = "USD" }: { price: number; currency?: string }) {
return (
<span className="text-2xl font-bold">
{new Intl.NumberFormat("en-US", { style: "currency", currency }).format(price)}
</span>
);
}
Always Client
"use client";
// Shopping cart — state, interaction
function CartButton() {
const { count, open } = useCart();
return (
<button onClick={open} className="relative">
Cart
{count > 0 && <span className="badge">{count}</span>}
</button>
);
}
// Any form with validation
"use client";
function EnrollmentForm({ courseId }: { courseId: string }) {
const [loading, setLoading] = useState(false);
// ...
}
// Dark mode toggle
"use client";
function ThemeToggle() {
const { theme, setTheme } = useTheme();
return <button onClick={() => setTheme(t => t === "dark" ? "light" : "dark")}>Toggle</button>;
}
// Third-party chart library
"use client";
import { LineChart } from "recharts"; // uses browser DOM
function RevenueChart({ data }: { data: Point[] }) {
return <LineChart data={data} />;
}
The Boundary Pattern
Push "use client" as deep as possible. Keep the data fetching in Server Components and only make the interactive pieces Client:
// ✅ Server Component fetches data
// src/app/courses/[slug]/page.tsx
export default async function CoursePage({ params }: Props) {
const course = await getCourse(params.slug);
const user = await getSession();
return (
<div>
<CourseHeader course={course} /> {/* Server Component */}
<CourseLessons lessons={course.lessons} /> {/* Server Component */}
{/* Only the enroll button needs client interactivity */}
<EnrollButton
courseId={course.id}
price={course.price}
isEnrolled={user?.enrollments.includes(course.id) ?? false}
/>
</div>
);
}
// ✅ Client Component is just the interactive part
"use client";
function EnrollButton({ courseId, price, isEnrolled }: Props) {
const [loading, setLoading] = useState(false);
async function handleEnroll() {
setLoading(true);
await fetch("/api/enroll", { method: "POST", body: JSON.stringify({ courseId }) });
setLoading(false);
}
if (isEnrolled) return <span>✓ Enrolled</span>;
return (
<button onClick={handleEnroll} disabled={loading}>
{loading ? "Processing..." : `Enroll for $${price}`}
</button>
);
}
Common Mistakes
Mistake 1: Adding "use client" to avoid Server Component limitations
// ❌ Don't do this just to avoid the async component pattern
"use client";
import { useEffect, useState } from "react";
function CourseList() {
const [courses, setCourses] = useState([]);
useEffect(() => {
fetch("/api/courses").then(r => r.json()).then(setCourses);
}, []);
return courses.map(c => <CourseCard key={c.id} course={c} />);
}
// ✅ Use a Server Component
async function CourseList() {
const courses = await db.course.findMany();
return courses.map(c => <CourseCard key={c.id} course={c} />);
}
Mistake 2: Making providers "use client" and putting Server Component children inside them
This is actually fine — Server Components can be children of Client Components:
// ✅ This works correctly
"use client";
export function ThemeProvider({ children }: { children: React.ReactNode }) {
// ...
return <ThemeContext.Provider value={...}>{children}</ThemeContext.Provider>;
}
// layout.tsx — Server Component
import { ThemeProvider } from "@/components/ThemeProvider";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
{children} {/* children are Server Components — this is fine */}
</ThemeProvider>
);
}
Mistake 3: Importing Server Components into Client Components
"use client";
// ❌ Can't import a Server Component into a Client Component
import { ServerOnlyComponent } from "./ServerOnly";
// ✅ Pass it as a prop (children) from a Server Component
Next lesson: Server Actions and form handling — calling server code directly from your forms.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises