React Server Components llegaron con promesas grandes, y con una confusión proporcional. Después de meses usando RSC en producción con Next.js App Router, la pregunta más común que recibo sigue siendo: "Pero ¿cuándo uso Server Component?"

Este artículo no es introducción, asume que ya sabes qué es RSC. Es una guía de decisión práctica: cuándo usar qué, por qué, y los errores que casi todo el mundo comete en la transición.

El modelo mental correcto

El error más común es pensar en RSC como "componentes que corren en el servidor" y Client Components como "componentes que corren en el cliente". Esa descripción es correcta pero incompleta de una forma que genera malas decisiones.

La forma más útil de pensar:

  • Server Components son funciones que corren apenas en el servidor, tienen acceso directo a datos, base, filesystem, APIs internas, y nunca son incluidas en el bundle del cliente. Producen HTML + RSC payload que es enviado al cliente.
  • Client Components son funciones que corren en el cliente, y también en el servidor para hidratación. Tienen acceso a state, effects, event handlers y APIs del browser. Son incluidas en el bundle.

La implicación práctica: Server Components no cargan JavaScript en el cliente. Cualquier lógica, dependencia o procesamiento hecho en un Server Component tiene costo cero de bundle.

Cuándo usar Server Components

Usa Server Components cuando el componente:

  • Busca datos directamente, base, API, filesystem
  • Usa dependencias grandes que no necesitan ser interactivas (marked, sharp, parsers pesados)
  • Renderiza contenido estático o con datos que vienen del servidor
  • Necesita acceso a variables de ambiente secretas
  • Es una capa de composición que no tiene estado ni interactividad propia
// Server Component: búsqueda directa, sin bundle en el cliente
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>
  )
}

Cuándo usar Client Components

Usa Client Components cuando el componente necesita:

  • Estado local (useState, useReducer)
  • Efectos (useEffect, useLayoutEffect)
  • Event handlers (onClick, onChange, etc.)
  • APIs del browser (localStorage, window, navigator)
  • Librerías que dependen de APIs del browser
'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)}>
        Agregar al carrito
      </button>
    </div>
  )
}

Los errores más comunes

1. Marcar todo como 'use client' por precaución

El error más común en la transición. Si agregas 'use client' en todos los componentes "solo para que funcione", perdiste el beneficio central de los RSC, reducción de bundle. Un Server Component importado dentro de un Client Component se vuelve un Client Component también.

2. Pasar objetos no serializables a Client Components

Server Components pueden pasar props a Client Components, pero solo props serializables, strings, números, arrays, objetos simples. Funciones, instancias de clase y objetos con métodos no pueden pasarse. Esto toma a mucha gente por sorpresa.

// ❌ Error: funciones no pueden pasarse de Server a Client
<ClientComponent handler={someServerFunction} />

// ✅ Correcto: define el handler en el propio Client Component
// o usa Server Actions para callbacks

3. Confundir Server Actions con Server Components

Server Actions son funciones asíncronas que corren en el servidor y pueden ser llamadas desde Client Components. Son el mecanismo para mutaciones, no para búsqueda de datos. Usa Server Components para lectura, Server Actions para escritura.

4. Waterfall de datos no intencional

Con async/await en Server Components, es fácil crear waterfalls:

// ❌ Waterfall: espera uno, después el otro
const user = await fetchUser(id)
const orders = await fetchOrders(user.id)

// ✅ Paralelo: búsqueda simultánea
const [user, orders] = await Promise.all([
  fetchUser(id),
  fetchOrders(id)
])

El patrón de composición que funciona

El patrón más eficiente es mantener lo máximo posible como Server Components, y empujar los Client Components hacia las hojas del árbol, los componentes más cercanos a la interactividad real.

Una página típica bien estructurada con RSC tiene: Server Component como raíz, busca datos, composición, Client Components solo donde hay interactividad, y Server Components anidados para sub-secciones que necesitan datos pero no interactividad.

Cuando interiorizas ese patrón, la arquitectura empieza a fluir, y el bundle del cliente se encoge.