Server Components

Hard

React Server Components execute on the server and send rendered output to the client with zero JavaScript bundle cost. They can directly access databases, file systems, and server APIs. Combined with Client Components for interactivity, they enable a hybrid architecture that minimizes client-side JavaScript.

Interactive Visualization

Key Points

  • Server Components run only on the server with no client-side JavaScript
  • They can directly access databases, file systems, and server-only APIs
  • Cannot use hooks, state, effects, or browser APIs
  • Client Components are marked with "use client" and include JavaScript in the bundle
  • Server Components can import and render Client Components
  • Client Components cannot import Server Components but can accept them as children

Code Examples

Server and Client Component Composition

// Server Component (default in Next.js App Router)
// No "use client" directive
async function ProductPage({ id }: { id: string }) {
  // Direct database access on the server
  const product = await db.products.findById(id)
  const reviews = await db.reviews.findByProduct(id)

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>Price: ${product.price}</p>

      {/* Client Component for interactivity */}
      <AddToCartButton productId={id} price={product.price} />

      {/* Server-rendered list with no client JS */}
      <ReviewList reviews={reviews} />
    </div>
  )
}

function ReviewList({ reviews }: { reviews: { id: string; text: string; rating: number }[] }) {
  return (
    <ul>
      {reviews.map((r) => (
        <li key={r.id}>
          {'★'.repeat(r.rating)} {r.text}
        </li>
      ))}
    </ul>
  )
}

The product page and review list are Server Components with zero client JS; only AddToCartButton ships JavaScript for the click handler

Client Component with use client

'use client'

import { useState } from 'react'

interface AddToCartButtonProps {
  productId: string
  price: number
}

function AddToCartButton({ productId, price }: AddToCartButtonProps) {
  const [added, setAdded] = useState(false)
  const [quantity, setQuantity] = useState(1)

  const handleAdd = async () => {
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId, quantity }),
    })
    setAdded(true)
  }

  if (added) return <span>Added to cart!</span>

  return (
    <div>
      <select
        value={quantity}
        onChange={(e) => setQuantity(Number(e.target.value))}
      >
        {[1, 2, 3, 4, 5].map((n) => (
          <option key={n} value={n}>{n}</option>
        ))}
      </select>
      <button onClick={handleAdd}>
        Add to Cart - ${(price * quantity).toFixed(2)}
      </button>
    </div>
  )
}

export { AddToCartButton }

The "use client" directive marks this as a Client Component that includes JavaScript for state management and event handling

Common Mistakes

  • Adding "use client" to components that do not need interactivity, inflating the bundle
  • Trying to use hooks or browser APIs in Server Components
  • Importing Server Components into Client Components instead of passing them as children

Interview Tips

  • Explain the serialization boundary between Server and Client Components
  • Know that Server Components can render Client Components but not vice versa
  • Discuss the bundle size benefits and the new mental model for deciding server vs client

Related Concepts