Server Components vs Client Components

intermediate
12 min

Server Components vs Client Components

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.

Server Components: The Default

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:

  • Zero bundle cost: a huge markdown parser used in a Server Component adds nothing to the client bundle.
  • Direct data access: query a database or read files with no API layer.
  • Secrets stay secret: API keys used on the server never reach the browser.
jsx
// 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.

Client Components: Opt In With 'use client'

When you need interactivity, add the 'use client' directive as the first line of the file:

jsx
'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.

Choosing: A Simple Decision Rule

Ask one question: does this component need interactivity or browser APIs?

  • Needs onClick, onChange, useState, useEffect, or window? → Client Component.
  • Just displays data or structure? → Server Component (the default).

Fetch data and render content on the server; sprinkle small Client Components where users actually interact.

The Boundary Rule

'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.

Composing the Two

A Client Component cannot import a Server Component directly - but it can receive one as children:

jsx
// 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 Must Be Serializable

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).

Key Takeaways

  • Everything is a Server Component by default; 'use client' opts a subtree into the client.
  • Server Components keep bundles small, access data directly, and protect secrets.
  • Client Components own state, effects, events, and browser APIs.
  • Push client boundaries to the leaves and pass Server Components through children.

Next up: Lesson 7 builds on this foundation with data fetching and the Next.js caching model.

Server Components vs Client Components - Next.js | CodeYourCraft | CodeYourCraft