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.
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.
Common client component candidates:
- Interactive forms with controlled inputs
- Dropdown menus, modals, tabs driven by local state
- Components that call
useEffectto 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. We design and ship production Next.js systems for founders and product teams.