Server Components vs Client Components
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.