# Server Components vs Client Components: The Mental Model That Finally Clicked
By Govind Bajaj · 2026

Tags: react, next.js, server-components, app-router, architecture
I taught Server Components to three junior devs last month. Two of them were confused by the official docs. Here's the one-sentence rule that made everything click.
I taught Server Components to three junior developers last month.

Two of them read the official docs and came back more confused than when they started. "I know when to use each one," one of them said, "but I don't understand *why*."

The docs explain the *what* well. But the *why* requires a mental model shift that took me months to internalize. Here's the shortcut.

## The One-Sentence Rule

**Server Components run where the data lives. Client Components run where the user lives.**

That's it. Every other rule derives from this.

Data lives on the server — in your database, in your file system, in your API responses, in your environment variables. The user lives on the client — they click, they type, they scroll, they resize the browser.

When you need data, use a Server Component. When you need interactivity, use a Client Component.

## Why This Matters More Than "Zero Bundle Size"

The official docs lead with "Server Components send zero JavaScript to the client." This is true but misleading as a primary mental model. It's an implementation detail, not a decision framework.

The real power is that Server Components can do things Client Components cannot:

- Read directly from your database (no API endpoint needed)
- Access server-only secrets and environment variables
- Read the filesystem
- Run at build time for static generation, or per-request for dynamic content

```tsx
// Server Component — can read directly from DB
import { db } from '@/lib/db'

export default async function ProductPage() {
  const products = await db.query.products.findMany()
  
  return (
    <div>
      {products.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  )
}
```

Notice there's no `useEffect`, no loading state, no error boundary for the data fetch. The component is async. It suspends at the data boundary. React handles the rest.

## The Boundary Pattern

In practice, most of your app should be Server Components. You sprinkle Client Components only where interactivity is needed.

```tsx
// Server Component shell
import { db } from '@/lib/db'
import { AddToCartButton } from './AddToCartButton' // Client Component

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.query.products.findFirst({
    where: { id: params.id }
  })
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      
      {/* Client Component for interactivity */}
      <AddToCartButton productId={product.id} />
    </div>
  )
}
```

```tsx
// Client Component — handles user interaction
'use client'

import { useState } from 'react'

export function AddToCartButton({ productId }: { productId: string }) {
  const [adding, setAdding] = useState(false)
  
  async function addToCart() {
    setAdding(true)
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId })
    })
    setAdding(false)
  }
  
  return (
    <button onClick={addToCart} disabled={adding}>
      {adding ? 'Adding...' : 'Add to Cart'}
    </button>
  )
}
```

The Server Component fetches data. The Client Component handles the button click. They compose together naturally.

## Common Anti-Patterns I See

**Anti-pattern 1: Marking everything 'use client'**

I've seen files with 200 lines of data fetching logic marked `'use client'` because there's one `onClick` handler at the bottom. Extract the interactive part into a small Client Component. Keep the data fetching in a Server Component.

**Anti-pattern 2: Fetching in useEffect from a Client Component**

```tsx
// Bad: Client Component fetching data it could get from a Server Component
'use client'

export function ProductList() {
  const [products, setProducts] = useState([])
  
  useEffect(() => {
    fetch('/api/products').then(r => r.json()).then(setProducts)
  }, [])
  
  return <div>{/* ... */}</div>
}
```

This creates a waterfall: the page loads, hydrates, then fires the fetch. A Server Component fetches during render, so the data arrives with the HTML.

**Anti-pattern 3: Passing server-only data through Client Components**

If you need a secret or a server-only module, don't pass it through a Client Component as a prop. Client Components can't use server-only modules, even if the parent passes the data. Keep the server-only logic in Server Components.

## The `'use server'` Directive for Mutations

Server Actions (functions marked with `'use server'`) let Client Components call server-side code directly.

```tsx
'use client'

import { updateQuantity } from './actions'

export function QuantitySelector({ itemId, initial }: { itemId: string, initial: number }) {
  return (
    <select onChange={e => updateQuantity(itemId, Number(e.target.value))}>
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
  )
}
```

```ts
// actions.ts
'use server'

import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'

export async function updateQuantity(itemId: string, quantity: number) {
  await db.update(cartItems).set({ quantity }).where(eq(cartItems.id, itemId))
  revalidatePath('/cart')
}
```

No API endpoint. No fetch call. No useState for the form. The select's onChange calls a server function directly.

## Takeaways

- Server Components run where the data lives; Client Components run where the user lives
- Most of your app should be Server Components; Client Components are the exception
- Never fetch in useEffect what you can fetch in a Server Component
- Server Actions eliminate boilerplate API routes for mutations
- Extract small interactive pieces into Client Components instead of marking entire pages 'use client'
- This mental model took me months to internalize; use it as your starting framework