32blogby StudioMitsu

React Server Components: Guía Completa

Entiende cómo funciona RSC, cuándo usar Server vs Client Components, y patrones prácticos para Next.js App Router.

8 min read
Contenido

Cuando los React Server Components (RSC) aparecieron, estaba genuinamente confundido. "¿No es esto simplemente SSR?" y "¿Dónde se supone que los escribo?" eran preguntas que no pude responder por un tiempo.

Este artículo explica RSC desde cero — en qué se diferencia de SSR, cuándo usar Server vs Client Components, y los patrones que usarás todos los días en un proyecto con Next.js App Router.

¿Qué Es RSC? ¿En Qué Se Diferencia de SSR?

La definición en una frase: RSC es un componente que se ejecuta solo en el servidor y envía cero JavaScript al cliente.

SSR vs RSC

SSR (Server-Side Rendering) genera HTML en el servidor, lo envía al cliente, y luego carga JavaScript para que React pueda "tomar el control" — un proceso llamado hidratación. Todo el código del componente se sigue enviando al navegador.

RSC es fundamentalmente diferente:

SSRRSC
EjecuciónServidor (HTML) + Cliente (hidratación)Solo servidor
JS enviado al clienteSí — todo el código del componenteNinguno
InteractividadSí (después de la hidratación)No
Acceso directo a BDRequiere soluciones alternativasFunciona de forma nativa
Impacto en el bundleCero

Los componentes RSC se ejecutan en el servidor, producen HTML y nunca aparecen como JavaScript en el bundle del cliente. Incluso las librerías npm pesadas que importas en RSC no añaden al tamaño del bundle.

El Problema que RSC Resuelve

tsx
// ❌ Traditional approach — data fetching on the client
"use client";
import { useEffect, useState } from "react";

function ProductList() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    fetch("/api/products").then(res => res.json()).then(setProducts);
  }, []);

  return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

Problemas aquí:

  • Solicitud extra de ida y vuelta desde el cliente (cascada)
  • Se necesita gestión de estado de carga
  • El JavaScript del componente infla el bundle
tsx
// ✅ RSC approach
// app/products/page.tsx (Server Component by default in App Router)
import { db } from "@/lib/db";

async function ProductList() {
  // Direct DB access — no API layer needed
  const products = await db.product.findMany();

  return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
  • Sin solicitudes extra
  • Sin necesidad de estado de carga
  • Cero JS enviado al cliente

Server vs Client Components: Cuándo Usar Cada Uno

En Next.js App Router, todos los componentes son Server Components por defecto. Optas por Client Components añadiendo la directiva "use client".

Cuándo Necesitas un Client Component

tsx
"use client"; // Required — without this, using hooks causes an error

import { useState, useEffect } from "react";

export function Counter() {
  const [count, setCount] = useState(0); // State required
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}

Usa Client Components cuando necesites:

  • Hooks de React (useState, useReducer, useEffect, etc.)
  • APIs del navegador (window, localStorage, navigator, etc.)
  • Manejadores de eventos (click, envío de formulario, scroll, etc.)
  • Animaciones (Framer Motion, etc.)

Cuando los Server Components Brillan

tsx
// app/blog/[slug]/page.tsx
import { db } from "@/lib/db";
import { marked } from "marked"; // Heavy library — zero bundle cost

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  // Direct DB query — no API route needed
  const post = await db.post.findUnique({
    where: { slug },
  });

  if (!post) return <div>Post not found</div>;

  // marked runs on the server only — no JS shipped to client
  const html = marked(post.content);

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: html }} />
    </article>
  );
}

Los Server Components son ideales cuando:

  • Obtienes datos de una base de datos o API externa
  • Necesitas acceder a secretos (claves API, tokens)
  • Quieres usar librerías grandes sin coste en el bundle
  • Solo necesitas renderizar HTML sin interacción del usuario

Composición de Server y Client Components

Aquí está la regla crítica: los Server Components pueden pasarse como children a Client Components, pero los Client Components no pueden importar directamente Server Components.

Patrón Que No Funciona

tsx
"use client";
// ❌ Importing a Server Component from a Client Component doesn't work
import { ServerSideWidget } from "./ServerSideWidget";

export function ClientShell() {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setOpen(o => !o)}>Toggle</button>
      {open && <ServerSideWidget />} {/* ❌ Won't work as expected */}
    </div>
  );
}

El Patrón Children

tsx
// ClientShell.tsx
"use client";

import { ReactNode, useState } from "react";

export function ClientShell({ children }: { children: ReactNode }) {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setOpen(o => !o)}>Toggle</button>
      {open && children} {/* ✅ Accept Server Component as children */}
    </div>
  );
}
tsx
// app/page.tsx (Server Component)
import { ClientShell } from "./ClientShell";
import { ServerSideWidget } from "./ServerSideWidget"; // Server Component

export default function Page() {
  return (
    // ✅ Compose in the Server Component layer
    <ClientShell>
      <ServerSideWidget />
    </ClientShell>
  );
}

El patrón children es el patrón de composición más importante en RSC. Crea "slots" (children) en Client Components y pasa Server Components dentro de ellos desde la capa de Server Component.

Estructura Típica de Proyecto

app/
├── page.tsx              # Server Component (data fetching)
├── components/
│   ├── ProductCard.tsx   # Server Component (static display)
│   ├── AddToCart.tsx     # "use client" (click handler needed)
│   └── SearchBox.tsx     # "use client" (input state)
tsx
// app/page.tsx
import { db } from "@/lib/db";
import { ProductCard } from "./components/ProductCard";
import { SearchBox } from "./components/SearchBox";

export default async function HomePage() {
  const products = await db.product.findMany({ take: 10 });

  return (
    <main>
      <SearchBox /> {/* Client Component */}
      <div className="grid grid-cols-3 gap-4">
        {products.map(product => (
          <ProductCard key={product.id} product={product}>
            {/* AddToCart is a Client Component, but product data flows from the server */}
            <AddToCart productId={product.id} />
          </ProductCard>
        ))}
      </div>
    </main>
  );
}

Server Actions

RSC se combina naturalmente con Server Actions — funciones del lado del servidor que manejan envíos de formularios y mutaciones.

tsx
// app/actions.ts
"use server"; // Server Action directive

import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";

export async function addProduct(formData: FormData) {
  const name = formData.get("name") as string;
  const price = Number(formData.get("price"));

  await db.product.create({ data: { name, price } });
  revalidatePath("/products"); // Invalidate cache to trigger re-render
}
tsx
// app/products/new/page.tsx (Server Component)
import { addProduct } from "../actions";

export default function NewProductPage() {
  return (
    <form action={addProduct}>
      <input name="name" placeholder="Product name" />
      <input name="price" type="number" placeholder="Price" />
      <button type="submit">Add Product</button>
    </form>
  );
}

Suspense y Streaming

RSC funciona con React Suspense para habilitar el renderizado por streaming. Puedes mostrar la página inmediatamente y transmitir las secciones costosas a medida que los datos están disponibles.

tsx
// app/dashboard/page.tsx
import { Suspense } from "react";
import { HeavyStats } from "./components/HeavyStats";

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* Renders immediately */}
      <QuickSummary />

      {/* Streams in when HeavyStats finishes fetching */}
      <Suspense fallback={<div>Loading stats...</div>}>
        <HeavyStats />
      </Suspense>
    </div>
  );
}
tsx
// app/dashboard/components/HeavyStats.tsx (Server Component)
async function HeavyStats() {
  // Even if this takes 2 seconds, the rest of the page is unblocked
  const stats = await fetchExpensiveStats();

  return (
    <div>
      <p>Revenue: {stats.revenue}</p>
      <p>Users: {stats.users}</p>
    </div>
  );
}

El esqueleto de la página se renderiza y se envía al navegador inmediatamente. HeavyStats llega por streaming cuando está listo — sin bloquear la carga completa de la página.

Conclusión

Los React Server Components son componentes que se ejecutan solo en el servidor y envían cero JavaScript al cliente.

Caso de UsoServer ComponentClient Component
Obtención de datosEvitar
Acceso directo a BDNo es posible
Secretos / claves APIPeligroso
useState / useEffectNo es posible
Manejadores de eventosNo es posible
APIs del navegadorNo es posible

En App Router, piensa "Server por defecto, Client solo cuando sea necesario". Cuando necesites hooks o manejadores de eventos, añade "use client" en ese punto.

Domina el patrón de composición con children, y podrás mezclar libremente Client Components interactivos con obtención de datos del servidor. RSC reduce el tamaño del bundle y elimina la mayoría del boilerplate de la capa API — definitivamente vale la pena entenderlo en profundidad.