Next.js: Server Components & Client Components

A calm, clear guide to understanding Server Components and Client Components in Next.js β€” what runs where, how they work together, and how to choose the right component for the job.

A calm, practical guide to understanding what runs on the server, what runs in the browser, and why this distinction matters in modern React development.

Server Components are one of the defining features of modern Next.js.
They represent a shift in how we think about rendering, data fetching, and performance.
But they also introduce new questions:

  • What exactly is a Server Component?
  • When do I need a Client Component?
  • How do they interact?
  • What patterns lead to stable, predictable apps?

In this chapter, we’ll clarify these ideas β€” simply, calmly, without hype.

Why Server Components exist

Traditional React renders everything on the client.
But this leads to unavoidable problems:

  • Large JavaScript bundles
  • Slow initial loads
  • Heavy hydration cycles
  • Data fetching complexity
  • Difficulty integrating with backend systems

Server Components solve these issues by allowing React to run at build time or on the server, generating lightweight HTML and minimizing work in the browser.

The goal is simple:

Move logic to the server when possible. Keep the browser lightweight and fast.

The key idea: components run in different environments

In the App Router:

  • Server Components run on the server (default)
  • Client Components run in the browser (opt-in)

This leads to a cleaner mental model:

Feature Server Component Client Component
Runs in browser? ❌ No βœ… Yes
Can use useState, useEffect, refs? ❌ No βœ… Yes
Can access DB, filesystem, private APIs? βœ… Yes ❌ No
Included in JS bundle? ❌ No βœ… Yes
Best for UI + data Interactivity

You choose client components only when you need interactivity β€” everything else stays server-side.

Server Components (default)

A simple Server Component looks like this:

export default async function Page() {
  const data = await getData();
  return <div>{data.title}</div>;
}

Characteristics:

  • No "use client" directive
  • Runs on the server
  • Can fetch data directly
  • Not included in JS bundles
  • Can be async
  • No state or effects

Server Components are ideal for:

  • Rendering database-driven UI
  • Fetching external API data
  • Static or semi-static content
  • Layouts
  • Lists, feeds, articles
  • Pages without interactivity

They keep your app fast and lean.

Client Components ("use client")

Client Components opt into the browser environment:

"use client";

import { useState } from "react";

export default function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

Characteristics:

  • Runs in browser
  • Can use state, effects, context, refs
  • Must be bundled
  • Cannot directly access database or secrets
  • Can import Server Components (but not the reverse)

Use Client Components when you need:

  • Buttons, toggles, interactive UI
  • Forms with client behavior
  • Animations
  • Event listeners
  • Web APIs (localStorage, window, etc.)

Client logic becomes the exception, not the default.

How Server and Client Components work together

React allows mixing both types inside the same UI tree.

Example:

<ServerSidebar>
  <ClientChart />
  <ClientDropdown />
</ServerSidebar>

Important rules:

  1. Server Components can import Client Components.
    This is the most common pattern.

  2. Client Components cannot import Server Components.
    This prevents server-only code from being bundled.

  3. Props between server β†’ client must be serializable.
    (JSON-compatible)

  4. Client Components can accept children that are Server Components, but only through special wrappers (e.g., <Suspense>).

This allows for elegant composition:

  • Server Components provide data
  • Client Components handle interactivity

Your app becomes β€œas client-heavy as necessary β€” and no more.”

Performance benefits of Server Components

Server Components significantly improve:

1. Initial load times

You ship less JavaScript, since server-only code never reaches the browser.

2. Data loading

Data is fetched directly on the server without waterfalls or client-side delays.

3. Caching

Requests can be memoized automatically by React, improving both speed and efficiency.

4. Security

Server Components can read environment variables, credentials, and databases β€” safely.

5. Streaming

UI can render progressively as data resolves.

This is why Server Components are the new default.

When to choose each component type

Below is a calm, practical guide:

Use Server Components for:

  • Pages that fetch data
  • Lists, tables, dashboards
  • Layouts and shells
  • SEO-heavy content
  • Blog posts, docs, articles
  • Anything that doesn’t need interactivity

This should be most of your codebase.

Use Client Components for:

  • Inputs, forms, and event handlers
  • Components with useState, useEffect, useRef
  • Widgets, modals, dropdowns
  • Realtime UI
  • External libraries that require the DOM

Only introduce them when necessary.

Patterns for real applications

Here are practical combinations that reflect real Birdor-style engineering.

Pattern 1: Server-driven page with client widget

app/
  dashboard/
    page.tsx      β†’ Server Component
    chart.tsx     β†’ Client Component
// page.tsx
import Chart from "./chart";

export default async function Page() {
  const stats = await loadStats();
  return <Chart data={stats} />;
}

A clean handoff: server loads data β†’ client visualizes it.

Pattern 2: Server layout with client navigation

layout.tsx   β†’ Server
nav.tsx      β†’ Client (for local state)

Good for global UI:

  • Sidebar
  • Header with dropdown
  • Mobile navigation

Pattern 3: Server page with client-enhanced sections

<ServerContent />
<ClientActions />
<ClientInteractiveTable />

The page stays fast; only interactive regions load JS.

Common pitfalls and how to avoid them

❌ Overusing Client Components

Fix: Default to server β€” add "use client" only when needed.

❌ Mixing server and client boundaries incorrectly

Remember: client β†’ server imports are not allowed.

❌ Fetching data inside Client Components

Prefer server-side fetching, then pass data as props.

❌ Using client components for pure layout

Layouts should almost always be server components.

❌ Trying to serialize complex class instances

Props must be serializable.

Stay mindful of boundaries, and your app stays predictable.

Summary

Server Components and Client Components give you a powerful way to design fast, modern web applications:

  • Server Components β†’ lean, secure, data-ready, fast
  • Client Components β†’ interactive, stateful, browser-driven

Use Server Components by default.
Add Client Components only when the UI truly needs them.

This balance keeps your app calm, maintainable, and production-ready.

Next, we’ll explore data fetching in Next.js β€” one of the most important topics in the App Router era.

In many ways, it’s the chapter that unlocks the whole system.

Keep Reading

Follow the engineering thread

Get the next practical Birdor note, or browse the archive for related systems, tooling, and architecture work.

Join newsletter Browse articles