React Server Components arrived with big promises, and with proportional confusion. After months using RSC in production with Next.js App Router, the most common question I still get is: "But when do I actually use a Server Component?"

This article is not an introduction, it assumes you already know what RSC is. It's a practical decision guide: when to use what, why, and the mistakes almost everyone makes in the transition.

The correct mental model

The most common mistake is thinking of RSC as "components that run on the server" and Client Components as "components that run on the client." That description is correct but incomplete in a way that generates poor decisions.

The most useful way to think about it:

  • Server Components are functions that run only on the server, have direct access to data, database, filesystem, internal APIs, and are never included in the client bundle. They produce HTML + RSC payload that is sent to the client.
  • Client Components are functions that run on the client, and also on the server for hydration. They have access to state, effects, event handlers, and browser APIs. They are included in the bundle.

The practical implication: Server Components don't ship JavaScript to the client. Any logic, dependency, or processing done in a Server Component has zero bundle cost.

When to use Server Components

Use Server Components when the component:

  • Fetches data directly, database, API, filesystem
  • Uses large dependencies that don't need to be interactive (marked, sharp, heavy parsers)
  • Renders static content or content with data from the server
  • Needs access to secret environment variables
  • Is a composition layer with no state or interactivity of its own
// Server Component: direct fetch, no client bundle
async function ProductPage({ id }: { id: string }) {
  const product = await db.product.findUnique({ where: { id } })

  return (
    <div>
      <h1>{product.name}</h1>
      <ProductActions product={product} /> {/* Client Component */}
    </div>
  )
}

When to use Client Components

Use Client Components when the component needs:

  • Local state (useState, useReducer)
  • Effects (useEffect, useLayoutEffect)
  • Event handlers (onClick, onChange, etc.)
  • Browser APIs (localStorage, window, navigator)
  • Libraries that depend on browser APIs
'use client'

import { useState } from 'react'

function ProductActions({ product }: { product: Product }) {
  const [quantity, setQuantity] = useState(1)

  return (
    <div>
      <input
        type="number"
        value={quantity}
        onChange={e => setQuantity(Number(e.target.value))}
      />
      <button onClick={() => addToCart(product.id, quantity)}>
        Add to cart
      </button>
    </div>
  )
}

The most common mistakes

1. Marking everything as 'use client' as a precaution

The most common mistake in the transition. If you add 'use client' to every component "just to make it work," you've lost the central benefit of RSC, bundle reduction. A Server Component imported inside a Client Component becomes a Client Component too.

2. Passing non-serializable objects to Client Components

Server Components can pass props to Client Components, but only serializable props, strings, numbers, arrays, plain objects. Functions, class instances, and objects with methods cannot be passed. This catches many people off guard.

// ❌ Error: functions can't be passed from Server to Client
<ClientComponent handler={someServerFunction} />

// ✅ Correct: define the handler in the Client Component itself
// or use Server Actions for callbacks

3. Confusing Server Actions with Server Components

Server Actions are async functions that run on the server and can be called from Client Components. They are the mechanism for mutations, not for data fetching. Use Server Components for reading, Server Actions for writing.

4. Unintentional data waterfall

With async/await in Server Components, it's easy to create waterfalls:

// ❌ Waterfall: waits for one, then the other
const user = await fetchUser(id)
const orders = await fetchOrders(user.id)

// ✅ Parallel: simultaneous fetching
const [user, orders] = await Promise.all([
  fetchUser(id),
  fetchOrders(id)
])

The composition pattern that works

The most efficient pattern is keeping as much as possible as Server Components, and pushing Client Components to the leaves of the tree, the components closest to real interactivity.

A typical well-structured page with RSC has: Server Component as root, data fetching, composition, Client Components only where there's interactivity, and nested Server Components for sub-sections that need data but not interactivity.

When you internalize this pattern, the architecture flows, and the client bundle shrinks.