Topic 03 / 13

Server Components vs Client Components

~11 min read  //  Next.js Series  //  Coding India

The Default: Server Components

Every component in app/ is a Server Component unless you opt out. Server components run only on the server: their code never ships to the browser, they can read databases and secrets directly, and they render to HTML.

// app/courses/page.js — server component (no directive needed)
import { db } from "@/lib/db";

export default async function CoursesPage() {
  const courses = await db.course.findMany();    // direct DB access!

  return (
    <ul>
      {courses.map(c => <li key={c.id}>{c.title}</li>)}
    </ul>
  );
}

Yes — async components that await data inline. No useEffect, no loading state management, no API round-trip from the browser.

What Server Components Can’t Do

They render once on the server, so anything interactive is off-limits: no useState/useEffect, no onClick, no browser APIs (localStorage, window). For that you need…

Client Components — “use client”

// components/AddToCart.js
"use client";                          // first line of the file

import { useState } from "react";

export default function AddToCart({ courseId }) {
  const [added, setAdded] = useState(false);

  return (
    <button onClick={() => { addToCart(courseId); setAdded(true); }}>
      {added ? "✓ In cart" : "Add to cart"}
    </button>
  );
}

"use client" marks the boundary: this component and everything it imports ships to the browser and hydrates. (Client components also pre-render to HTML on the server — “client” means “hydrates and runs in the browser”, not “skips SSR”.)

Composing Them

The pattern: server components for data and structure, client components as small interactive islands:

// server component — fetches data, renders the page
export default async function CoursePage({ params }) {
  const course = await getCourse(params.slug);

  return (
    <article>
      <h1>{course.title}</h1>
      <p>{course.description}</p>
      <AddToCart courseId={course.id} />     {/* client island */}
    </article>
  );
}

Props from server → client must be serialisable (plain objects, strings, numbers — no functions, no class instances).

The Slot Trick

A client component can’t import a server component — but it can receive one as children:

// ThemeProvider is "use client", yet wraps server-rendered pages fine:
<ThemeProvider>
  {children}        {/* server components pass through as a slot */}
</ThemeProvider>

That’s why providers in the root layout don’t turn your whole app into client components.

Choosing, at a Glance

  • Server (default): fetching data, markup, SEO content, anything using secrets.
  • Client: state, event handlers, effects, browser APIs, third-party interactive widgets.

Habit to build: keep "use client" as low in the tree as possible — a tiny <LikeButton>, not the whole page. Less shipped JS, faster pages.