shahriyar.dev
Back to blog
ReactNext.jsServer ComponentsClient ComponentsPerformanceWeb Development

Server Components vs Client Components: when to actually use each

·7 min read

If you've worked with React or Next.js recently, you've probably heard about the paradigm shift that Server Components introduced. The mental model of "everything renders on the client" is being replaced by a more nuanced approach where components can render on the server, on the client, or both. The decision between Server Components and Client Components isn't just a technical preference—it directly affects performance, bundle size, user experience, and even how you think about state and data fetching. Yet many developers struggle with knowing which one to pick when building a new feature. This post walks through the practical trade-offs and gives you a clear framework for making that choice.

The core difference at a glance

A Server Component renders once on the server and sends static HTML to the client. It never re-renders in the browser and has no access to interactivity, state, or effects. A Client Component renders on the server as well during the initial visit (for hydration), but it can also run and re-render in the browser in response to user interactions, state changes, or lifecycle events.

tsx
// Server Component (default in Next.js App Router)
export default async function ProductList() {
  const products = await fetch('...');
  const data = await products.json();
  return <ul>{data.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

// Client Component (must have 'use client' directive)
'use client';
import { useState } from 'react';
export default function LikeButton({ productId }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>;
}

The key takeaway: if your component needs interactivity (event handlers, hooks like useState, useEffect, or browser-only APIs), it must be a Client Component. Otherwise, it should default to a Server Component.

When to use Server Components

Data fetching and static content

Server Components shine when you need to fetch data that doesn't depend on user input. Because the fetch runs on the server, you avoid making additional round trips from the browser, and you can keep sensitive data (like API keys) out of client bundles.

  • Fetching from a database or a private API
  • Reading from a CMS or file system
  • Rendering markdown, blog posts, or product catalogues
  • Generating metadata or SEO-friendly HTML

Key benefits:

  • Zero client-side JavaScript for that component – smaller bundle, faster load.
  • Direct access to server resources – databases, file system, secrets.
  • Automatic streaming – you can send parts of the page progressively.
tsx
// Server Component – fetches on server, no JS sent to client
export default async function Post({ slug }) {
  const content = await getPostContent(slug); // server-side fetch
  return <article>{content}</article>;
}

When you need to avoid client-side cost

If a component appears far down the render tree or is only visible after user action (e.g., modals that open on click), a Server Component that renders static markup can be sent during initial load. The client never pays for its JavaScript until it's needed.

When to use Client Components

Interactivity is non-negotiable

Any component that handles user input—clicks, typing, scrolling, drag-and-drop, form submission—must be a Client Component. This includes:

  • Buttons, toggles, dropdowns
  • Text inputs, search bars, select fields
  • Interactive charts, maps, or data-visualisation widgets
  • Components that use useState, useReducer, useRef, useContext (with client-side state)

Note: You don't need to make the whole page a Client Component. Only the interactive leaf components need the 'use client' directive. The rest of the page can stay as Server Components that render the interactive children.

tsx
// Good: small Client Component embedded inside a Server Component
'use client';
export function ExpandButton({ children }) {
  const [expanded, setExpanded] = useState(false);
  return (
    <div>
      <button onClick={() => setExpanded(!expanded)}>Toggle</button>
      {expanded && children}
    </div>
  );
}

Browser-only APIs or side effects

If your component uses window, document, localStorage, navigator, or any browser API that doesn't exist on the server, it must be a Client Component. Effects like useEffect that subscribe to events, run timers, or connect to third-party SDKs also require the client.

tsx
'use client';
import { useEffect, useState } from 'react';
export function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const handler = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);
  return <p>Width: {width}px</p>;
}

Complex state management across the tree

While Server Components can hold state (like URL params or cookies), they cannot hold interactive client state. If your component needs to manage complex state that spans multiple sibling components, or if you're using a state management library like Zustand or Redux, that entire state tree lives inside a Client Component boundary.

The most common mistake: making everything a Client Component

Many developers coming from traditional React add 'use client' to every component out of habit. This defeats the purpose of Server Components. Every Client Component adds JavaScript to the client bundle, increases hydration time, and can block the main thread during loading. The rule of thumb should be:

  1. Start with a Server Component.
  2. Only add 'use client' when you absolutely need interactivity, state, or browser APIs.
  3. Push the 'use client' boundary as low as possible, wrapping only the interactive parts.

How to decide: a quick decision flowchart

Ask these questions in order:

  • Does the component need event handlers (onClick, onChange, etc.)?
    → Yes → Client Component
    → No → Continue
  • Does it use state hooks (useState, useReducer) or lifecycle hooks (useEffect, useLayoutEffect)?
    → Yes → Client Component
    → No → Continue
  • Does it rely on browser-only APIs (window, document, localStorage)?
    → Yes → Client Component
    → No → Continue
  • Is the data fetched from a server resource, or does the component render static/always-the-same markup?
    → Yes → Server Component (default)
    → No → Ask whether the component needs to be on the client at all (maybe it's purely presentational)

If you answered "No" to all three first questions, you almost certainly want a Server Component. This simple mental model will help you avoid over‑shipping JavaScript and keep your app fast.

Mixing Server and Client Components in practice

You can (and should) nest Server Components inside Client Components, and vice versa. But be careful: if you import a Server Component into a Client Component, it loses its server-only benefits and becomes a Client Component too (it gets bundled and hydrated). To pass Server Components into Client Components, use children or slot props.

tsx
// Server Component page
export default async function Page() {
  return (
    <InteractiveWrapper>
      {/* This content stays as a Server Component */}
      <StaticContent />
    </InteractiveWrapper>
  );
}

// Client Component wrapper
'use client';
export function InteractiveWrapper({ children }) {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button onClick={() => setOpen(!open)}>Toggle</button>
      {open && children}
    </>
  );
}

Here StaticContent is still rendered on the server, and only the wrapper and its toggle button become client JavaScript. This pattern keeps your client bundle lean.

Bottom line

Server Components are not a replacement for Client Components; they are a complement. Use Server Components for anything that can be pre‑rendered and doesn't need interactivity. Use Client Components sparingly, only for the parts that truly require a browser runtime. By being deliberate about the boundary, you can dramatically reduce JavaScript payloads, improve Time to Interactive, and still deliver rich, interactive UIs.

Next time you create a component, ask yourself: "Does this really need to run in the browser?" If the answer is no, let the server handle it. Your users (and your Lighthouse scores) will thank you.

Comments