Skip to content
Next.js6 min read

Server vs Client Components: Where to Draw the Line

React Server Components change how you structure a Next.js app. Here is a practical framework for deciding which components stay on the server and which need the client.

Every component in the App Router is a Server Component until you say otherwise. That single default is responsible for most of the confusion we see in code reviews: developers sprinkle 'use client' at the top of files until the errors go away, and end up shipping far more JavaScript than they need to. Drawing the boundary well is not difficult, but it does require a rule you can apply consistently.

What each kind is actually for

  • Server Components: fetching data, reading secrets, rendering markup, anything that does not need to run in the browser
  • Client Components: state, effects, event handlers, browser APIs, and third-party libraries that assume a DOM
  • Server Components add zero JavaScript to the bundle; Client Components ship their code and their dependencies

Notice what is missing from the client list: rendering. A component that merely displays data, however complex the markup, has no reason to be a Client Component. Interactivity is the trigger, not visual complexity.

One naming wrinkle is worth clearing up early: Client Components are still server-rendered for the initial HTML. The directive does not mean rendered only in the browser — it means hydrated and interactive in the browser. That removes half the fear of the boundary, because marking a component as client does not sacrifice server rendering or SEO; it costs bundle size and hydration work instead.

Push client boundaries to the leaves

The 'use client' directive does not mark one component — it marks a boundary. Everything imported below it becomes client code too. Put the directive on a page and you have effectively opted the whole route out of Server Components. The discipline that works is to isolate interactivity into the smallest component that needs it, and keep the parent on the server.

tsx
'use client';

import { useState } from 'react';

export function AddToCartButton({ productId }: { productId: string }) {
  const [pending, setPending] = useState(false);
  // event handlers and state live here, nowhere else
  return <button disabled={pending}>Add to cart</button>;
}

The product page that renders this button stays a Server Component. It fetches the product, renders the description and pricing, and drops in one small interactive island. The bundle contains the button, not the page.

Threading server content through client shells

A Client Component cannot import a Server Component, but it can receive one as children. This is the pattern that saves you when a layout-level component needs interactivity — a collapsible sidebar, a tabbed container, an animated wrapper — but its contents are server-rendered.

tsx
// Sidebar.tsx is a Client Component with open/close state
// page.tsx stays on the server:
export default async function Page() {
  const nav = await getNavigation();
  return (
    <Sidebar>
      <NavTree items={nav} />
    </Sidebar>
  );
}

Mistakes we keep seeing

  • Marking a whole page 'use client' because one form on it needs state
  • Passing non-serialisable values, such as functions or class instances, across the boundary as props
  • Importing a heavy charting or editor library into a shared component that most routes never use
  • Treating context providers as a reason to make everything a client — wrap once near the root and keep children on the server

A checklist that holds up

Ask three questions of every component. Does it handle events or hold state? Does it use browser-only APIs? Does it depend on a client-only library? Three noes means it stays on the server. One yes means you extract the smallest possible client island. Applied consistently, this keeps bundles small and pages fast without any heroics.

Interactivity is the trigger for a client boundary — never visual complexity, and never habit.

The boundary is an architectural decision, and like most architectural decisions it is cheap to get right early and expensive to fix later. Audit your 'use client' directives every so often; they have a habit of creeping upwards through the tree.

#nextjs#server components#architecture#performance
All articles

Start your project

Have an idea? Let's ship it together.

Tell us what you're building — we'll reply within one business day with an honest take and a clear next step.