Después de correr RSC en producción con Next.js App Router, la duda que más vuelve en conversación con otros equipos es sobre la frontera entre Server y Client Component. Abajo está el mapa de decisión que uso, y los errores que vi cometer al equipo en la transición.

Un Server Component renderiza en el servidor, consulta la base, el filesystem o una API interna, produce HTML más RSC payload, y no envía una línea de JavaScript al cliente. Un Client Component entra al bundle del cliente, corre en el navegador (y también en el servidor, en la hidratación), y accede a useState, useEffect, handlers de evento y APIs del browser.

La consecuencia práctica: la lógica, los parsers pesados y las llamadas a la base dentro de un Server Component cuestan cero bytes en el cliente. El manual de Server Components de React y la página de use client definen esa frontera.

Server Component cuando

  • El componente consulta datos directo: base, API interna, filesystem.
  • Usa una dependencia pesada sin interactividad (marked, sharp, un parser de PDF).
  • Lee una variable de ambiente que no puede ir al cliente.
  • Compone la página y no tiene estado propio.
// Server Component: búsqueda directa, sin JavaScript 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} />
    </div>
  )
}

Client Component cuando

  • Estado local: useState, useReducer.
  • Efectos: useEffect, useLayoutEffect.
  • Handlers de evento: onClick, onChange.
  • APIs del browser: localStorage, window, navigator.
'use client'

import { useState } from 'react'

export 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 que comete el equipo

1. 'use client' en todo componente

Una directiva de precaución convierte el árbol entero en Client Components y devuelve el bundle que los RSC existen para quitar. Un Server Component importado por un Client Component se vuelve Client Component también. Pon la directiva en la hoja que necesita interactividad y mantén el resto en el servidor.

2. Prop que no cruza la frontera

Un Server Component pasa prop a un Client Component solo si la prop serializa: string, número, array, objeto simple. Función, instancia de clase y objeto con métodos no cruzan, y el error aparece en runtime con el mensaje de serialización de React.

// error: la función no cruza la frontera
<ClientComponent handler={someServerFunction} />

// correcto: define el handler en el Client Component
// o llama a una Server Function desde él

3. Server Function confundida con búsqueda

Las Server Functions corren en el servidor y aceptan llamadas de Client Components. Sirven para mutaciones. La lectura queda con los Server Components; la escritura pasa por Server Functions.

4. La cascata de await

async/await en el cuerpo de un Server Component crea una cascata cuando una llamada depende de la otra por error:

// cascata: la segunda llamada espera a la primera
const user = await fetchUser(id)
const orders = await fetchOrders(user.id)

// paralelo: las dos salen juntas
const [user, orders] = await Promise.all([
  fetchUser(id),
  fetchOrders(id)
])

Cuando los datos no dependen uno del otro, Promise.all elimina el tiempo muerto entre las dos llamadas. Cuando la dependencia es real, revisa si la segunda llamada recibe el id directo, sin pasar por el objeto de la primera.

El patrón de composición

Una página con RSC bien armada tiene un Server Component como raíz, que hace la búsqueda y compone la estructura. Los Client Components quedan en las hojas, cerca de la interactividad. Las subsecciones que necesitan datos y no interactividad vuelven a ser Server Components anidados. La página de patrones de composición de Next.js muestra ese diseño con ejemplos de layout, Suspense y streaming.