Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
16 minLesson 19 of 33
Server & Client Components

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 async and await data 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

Get Notes Free →
!