Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
20 minLesson 25 of 40
Next.js 15 App Router

Server vs Client Components

Server vs. Client Components

The single biggest conceptual shift in Next.js App Router is understanding the difference between Server Components and Client Components. Get this right and everything else clicks into place. Get it wrong and you'll fight against the framework constantly.

The Default: Server Components

In the App Router, every component is a Server Component by default. Server Components run on the server — during the request, or at build time during static generation. They never run in the browser.

// src/app/courses/page.tsx
// This is a Server Component — no "use client" directive
import { getCourses } from "@/lib/courses";

export default async function CoursesPage() {
    // Can await directly — this runs on the server
    const courses = await getCourses();
    
    return (
        <main>
            <h1>All Courses</h1>
            {courses.map(course => (
                <div key={course.id}>{course.title}</div>
            ))}
        </main>
    );
}

No useEffect, no loading spinner, no client-side fetch. The data is fetched on the server and the HTML arrives ready in the response.

Client Components

Add "use client" at the top of the file to mark it as a Client Component. Client Components run in the browser and can use React state, effects, and browser APIs:

"use client";

import { useState } from "react";

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

"use client" marks a boundary. The component and everything it imports is bundled and sent to the browser.

When to Use Each

Server Component when you need to:

  • Fetch data from a database or API
  • Read files, environment variables, or secrets
  • Render content that doesn't need interactivity
  • Reduce the JavaScript sent to the browser

Client Component when you need:

  • useState, useReducer, or any React hooks
  • Event listeners (onClick, onChange, etc.)
  • Browser APIs (localStorage, window, navigator)
  • Real-time updates (subscriptions, WebSockets)
  • Third-party libraries that use browser features

What Server Components Can't Do

// ❌ This breaks — useState is not available in Server Components
import { useState } from "react";   // ERROR in Server Component

export default function ServerComponent() {
    const [open, setOpen] = useState(false);   // ❌
    
    return <button onClick={() => setOpen(true)}>Click</button>;  // ❌
}
// ❌ Also breaks — useEffect runs in the browser, not the server
import { useEffect } from "react";

export default function ServerComponent() {
    useEffect(() => {
        document.title = "Hello";   // ❌ no DOM on the server
    }, []);
}

The Composition Pattern

The key insight: you can nest Client Components inside Server Components, but not the other way around (you can't import a Server Component into a Client Component).

This means the recommended pattern is to keep most of your tree as Server Components and push "use client" down to the smallest interactive parts:

// src/app/courses/[slug]/page.tsx — Server Component
import { getCourse } from "@/lib/courses";
import EnrollButton from "@/components/EnrollButton";  // Client Component

export default async function CoursePage({ params }: { params: { slug: string } }) {
    const course = await getCourse(params.slug);
    
    return (
        <div>
            <h1>{course.title}</h1>
            <p>{course.description}</p>
            <p>Lessons: {course.lessonCount}</p>
            
            {/* Client Component receives serializable props */}
            <EnrollButton courseId={course.id} price={course.price} />
        </div>
    );
}
// src/components/EnrollButton.tsx — Client Component
"use client";

import { useState } from "react";

export default function EnrollButton({ courseId, price }: { courseId: string; price: number }) {
    const [loading, setLoading] = useState(false);
    
    async function handleEnroll() {
        setLoading(true);
        await fetch("/api/enroll", {
            method: "POST",
            body: JSON.stringify({ courseId }),
        });
        setLoading(false);
    }
    
    return (
        <button onClick={handleEnroll} disabled={loading}>
            {loading ? "Processing..." : `Enroll for $${price}`}
        </button>
    );
}

The Server Component fetches data. The Client Component handles the click. Each does what it's designed for.

Passing Data Down

Server Components can pass data to Client Components as props, but only serializable values (strings, numbers, arrays, plain objects). You cannot pass functions or class instances:

// Server Component passes plain data ✅
<ClientComponent 
    title="React Course"
    count={42}
    tags={["react", "javascript"]}
/>

// ❌ Cannot pass functions from Server to Client
<ClientComponent onComplete={() => doSomething()} />

Context in the App Router

You can't use React Context in Server Components. Context lives in the Client Component tree. Create a context provider as a Client Component and wrap your layout:

// src/components/ThemeProvider.tsx
"use client";

import { createContext, useContext, useState } from "react";

const ThemeContext = createContext({ isDark: false, toggle: () => {} });

export function ThemeProvider({ children }: { children: React.ReactNode }) {
    const [isDark, setIsDark] = useState(false);
    
    return (
        <ThemeContext.Provider value={{ isDark, toggle: () => setIsDark(d => !d) }}>
            {children}
        </ThemeContext.Provider>
    );
}

export function useTheme() {
    return useContext(ThemeContext);
}
// src/app/layout.tsx — Server Component wraps a Client Provider
import { ThemeProvider } from "@/components/ThemeProvider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html>
            <body>
                <ThemeProvider>
                    {children}   {/* Server Components can be children of Client Providers */}
                </ThemeProvider>
            </body>
        </html>
    );
}

The children passed into ThemeProvider are Server Components — they run on the server before being slotted into the Client Component's output. This is the correct pattern.

Third-Party Libraries

Many npm packages are written for the browser and don't work as Server Components. If you import a library that uses useState, useEffect, or browser APIs, you'll get an error unless you mark your component "use client".

// ❌ Error — toast library uses browser APIs
import { toast } from "sonner";

export default function Page() {
    toast("Hello");  // Fails — no browser in Server Component
}

// ✅ Wrap it in a Client Component
"use client";

import { toast } from "sonner";

export function NotifyButton() {
    return <button onClick={() => toast("Saved!")}>Save</button>;
}

Quick Decision Guide

Does the component need:
  ├─ onClick, onChange, onSubmit → "use client"
  ├─ useState, useEffect, useRef → "use client"
  ├─ window, document, localStorage → "use client"
  ├─ A third-party UI component (modals, dropdowns) → "use client"
  │
  ├─ Database query → Server Component (no "use client")
  ├─ File system read → Server Component
  ├─ Environment variable (secret) → Server Component
  └─ Static markup / no interactivity → Server Component (default)

When in doubt, start without "use client". You'll get a clear error if you try to use something that requires the browser. Add "use client" only when the error tells you to — or when you consciously need interactivity.

Next lesson: Data fetching in Next.js — fetch on the server, cache intelligently, and keep your UI fast.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!