If you understand one concept deeply in modern Next.js, make it this one. The App Router is built on React Server Components, and every component in your app is either a Server Component or a Client Component. Choosing correctly affects bundle size, performance, security, and what APIs you can use.
Every component in the app directory is a Server Component unless you say otherwise. Server Components run only on the server. Their JavaScript never ships to the browser - the browser receives just the rendered result.
That unlocks real advantages:
// app/posts/page.js - Server Component
import { db } from '@/lib/db';
export default async function PostsPage() {
const posts = await db.post.findMany(); // direct DB query
return (
<ul>
{posts.map((p) => <li key={p.id}>{p.title}</li>)}
</ul>
);
}The trade-off: Server Components cannot use state, effects, event handlers, or browser APIs. They render once on the server and are done.
When you need interactivity, add the 'use client' directive as the first line of the file:
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}Client Components can use useState, useEffect, event handlers, and browser APIs like localStorage. Note that they still render on the server first to produce initial HTML, then hydrate in the browser - 'client' means they also run on the client, not that they skip the server.
Ask one question: does this component need interactivity or browser APIs?
Fetch data and render content on the server; sprinkle small Client Components where users actually interact.
'use client' marks a boundary: every module imported by a Client Component becomes client code too. You do not need the directive in every interactive file - only at the entry point of a client subtree. Because of this, push Client Components to the leaves of your tree. Make the button a Client Component, not the whole page.
A Client Component cannot import a Server Component directly - but it can receive one as children:
// app/page.js - Server Component
import Modal from './Modal'; // client
import ServerInfo from './ServerInfo'; // server
export default function Page() {
return (
<Modal>
<ServerInfo /> {/* server-rendered, passed as children */}
</Modal>
);
}This pattern lets interactive shells wrap server-rendered content, keeping heavy work on the server.
Props passed from Server to Client Components cross a network boundary, so they must be serializable: strings, numbers, plain objects, arrays. You cannot pass functions or class instances (Server Actions are the exception, covered later).
Next up: Lesson 7 builds on this foundation with data fetching and the Next.js caching model.