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:
| SSR | RSC | |
|---|---|---|
| Ejecución | Servidor (HTML) + Cliente (hidratación) | Solo servidor |
| JS enviado al cliente | Sí — todo el código del componente | Ninguno |
| Interactividad | Sí (después de la hidratación) | No |
| Acceso directo a BD | Requiere soluciones alternativas | Funciona de forma nativa |
| Impacto en el bundle | Sí | Cero |
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
// ❌ 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
// ✅ 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
"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
// 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
"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
// 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>
);
}
// 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)
// 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.
// 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
}
// 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.
// 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>
);
}
// 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 Uso | Server Component | Client Component |
|---|---|---|
| Obtención de datos | ✅ | Evitar |
| Acceso directo a BD | ✅ | No es posible |
| Secretos / claves API | ✅ | Peligroso |
| useState / useEffect | No es posible | ✅ |
| Manejadores de eventos | No es posible | ✅ |
| APIs del navegador | No 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.