# Next.js Server Components vs Client Components: When to Use Each

> A practical breakdown of Next.js server components vs client components in the App Router — what each does, when to reach for it, and the mistakes to avoid.

- **Published:** 2026-01-01
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** next.js, server-components, app-router, react
- **Canonical URL:** https://clixo.sh/blog/nextjs-server-components-vs-client-components

Every team that moves to the Next.js App Router hits the same wall early: the page works, but they've quietly turned every component into a client component by adding `use client` to the top of the layout. The bundle balloons, server-side data access disappears, and the performance gains they came for are gone. The root problem is almost always a fuzzy mental model of where the boundary sits.

This post draws that boundary clearly — what server components are, what client components are, when each one belongs, and how to compose them without breaking either side.

## What Server Components Actually Do

Server components render on the server and send the result to the client as HTML (and a lightweight React tree for reconciliation). They never ship their own JavaScript to the browser.

Because they live on the server, they can:

- Query a database directly using an ORM
- Read environment variables and secrets without exposing them
- Import server-only Node.js modules
- Access the filesystem

They cannot:

- Use browser APIs (`window`, `document`, `localStorage`)
- Use React state (`useState`) or lifecycle hooks (`useEffect`)
- Register event handlers (`onClick`, `onSubmit`)
- Use context that depends on client state

In the App Router, **every component is a server component by default**. You opt into the client only when you have a specific reason to.

## What Client Components Actually Do

Client components are the React you've always written. They hydrate in the browser and can respond to user events, manage local state, and read browser APIs.

Adding `use client` at the top of a file marks the entire module — and everything it imports — as part of the client bundle. That directive does not make a component "dynamic" in the Next.js sense. It just tells the bundler to ship that code to the browser.

## The Decision Rule

Ask one question: **does this component need to react to user input or read browser state?**

If yes — reach for `use client`. If no — leave it as a server component.

```mermaid
flowchart TD
  A["New Component"] --> B{"Needs user interaction or browser API?"}
  B -->|No| C["Server Component — default"]
  B -->|Yes| D["Add 'use client'"]
  C --> E["Fetch data, read secrets, query DB"]
  D --> F{"Also needs data?"}
  F -->|Yes| G["Pass data as props from Server Component parent"]
  F -->|No| H["Client leaf — useState, event handlers"]
```

Common client component candidates:

- Interactive forms with controlled inputs
- Dropdown menus, modals, tabs driven by local state
- Components that call `useEffect` to subscribe to events
- Anything using third-party libraries that depend on `window`

Everything else — layouts, data-fetching wrappers, static text blocks, navigation shells — should stay on the server.

## How to Compose Them Correctly

### Push the boundary down

The most common mistake is putting `use client` on a page or a high-level layout. This converts every component in that subtree into a client component, which defeats the purpose of the App Router.

Instead, isolate interactivity into small leaf components. A product page can be a server component that fetches data, renders the product description, and drops in a single client component `AddToCartButton` at the leaf.

### Pass server data as props, not JSX children

You can pass server component output into a client component as children. A pattern that works well:

```
// page.tsx (server component)
import { InteractiveShell } from './interactive-shell'
import { getProduct } from '@/lib/db'

export default async function ProductPage({ params }) {
  const product = await getProduct(params.id)
  return (
    <InteractiveShell>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </InteractiveShell>
  )
}
```

`InteractiveShell` is a client component but its `children` — the product data rendered as HTML — came from the server and are never re-fetched on the client.

### Never import server-only code from a client component

If you import a module that uses `fs` or a database driver inside a `use client` file, Next.js will either error at build time or silently strip the code. Use the `server-only` package to make the boundary explicit and catch violations early.

```
// lib/db.ts
import 'server-only'
export async function getProduct(id: string) { ... }
```

## The `use server` Confusion

`use server` at the top of a function marks a **Server Action** — a callable server-side function exposed as a POST endpoint. It does not make a component a server component. Components are server components by default unless marked otherwise. Actions are the mechanism for mutations from client components.

Confusing these two directives is one of the most disorienting early mistakes teams make.

## Performance Implications

Server components reduce bundle size because their code never ships to the client. A complex data transformation done in a server component costs nothing in JavaScript weight. The same logic in a client component adds to the initial JS payload and runs again in the browser.

This also affects Time to First Byte. Server components can start streaming HTML immediately. Client components must wait for the JavaScript bundle to download and hydrate before they're interactive.

The practical advice: default to server, reach for `use client` only when the component has a clear interactive job.

## A Quick Reference

| Capability | Server Component | Client Component |
|---|---|---|
| Database / ORM access | Yes | No |
| Secret env vars | Yes | No |
| `useState` / `useEffect` | No | Yes |
| Event handlers | No | Yes |
| Browser APIs | No | Yes |
| Streaming / Suspense | Yes | Yes |

Getting this split right is one of the highest-leverage decisions in a Next.js App Router project. It drives bundle size, data security, and rendering performance simultaneously.

If you're building a product on Next.js and want an engineering team that gets these tradeoffs right from the start, [talk to Clixo](https://clixo.sh/#contact). We design and ship production Next.js systems for founders and product teams.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
