Los React Server Components (RSC) son componentes que se ejecutan exclusivamente en el servidor, enviando cero JavaScript al cliente — reduciendo el tamaño del bundle y permitiendo acceso directo a la base de datos sin una capa API.
Cuando RSC apareció, 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. La documentación de React los describe como componentes que se "pre-renderizan" por adelantado, en un entorno separado del 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
// ❌ Enfoque tradicional — obtención de datos en el cliente
"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
// ✅ Enfoque RSC
// app/products/page.tsx (Server Component por defecto en App Router)
import { db } from "@/lib/db";
async function ProductList() {
// Acceso directo a la BD — no se necesita capa API
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"; // Obligatorio — sin esto, usar hooks causa un error
import { useState, useEffect } from "react";
export function Counter() {
const [count, setCount] = useState(0); // Se necesita estado
return (
<button onClick={() => setCount(c => c + 1)}>
Contador: {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 (Motion, etc.)
Cuando los Server Components Brillan
// app/blog/[slug]/page.tsx
import { db } from "@/lib/db";
import { marked } from "marked"; // Librería pesada — cero coste en el bundle
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// Consulta directa a la BD — no se necesita ruta API
const post = await db.post.findUnique({
where: { slug },
});
if (!post) return <div>Artículo no encontrado</div>;
// marked se ejecuta solo en el servidor — no se envía JS al cliente
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";
// ❌ Importar un Server Component desde un Client Component no funciona
import { ServerSideWidget } from "./ServerSideWidget";
export function ClientShell() {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(o => !o)}>Alternar</button>
{open && <ServerSideWidget />} {/* ❌ No funcionará como se espera */}
</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)}>Alternar</button>
{open && children} {/* ✅ Aceptar Server Component como children */}
</div>
);
}
// app/page.tsx (Server Component)
import { ClientShell } from "./ClientShell";
import { ServerSideWidget } from "./ServerSideWidget"; // Server Component
export default function Page() {
return (
// ✅ Componer en la capa de Server Component
<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 (obtención de datos)
├── components/
│ ├── ProductCard.tsx # Server Component (visualización estática)
│ ├── AddToCart.tsx # "use client" (necesita manejador de click)
│ └── SearchBox.tsx # "use client" (estado del input)
// 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 es un Client Component, pero los datos del producto fluyen desde el servidor */}
<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"; // Directiva de Server Action
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"); // Invalidar caché para re-renderizar
}
// app/products/new/page.tsx (Server Component)
import { addProduct } from "../actions";
export default function NewProductPage() {
return (
<form action={addProduct}>
<input name="name" placeholder="Nombre del producto" />
<input name="price" type="number" placeholder="Precio" />
<button type="submit">Agregar producto</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>
{/* Se renderiza inmediatamente */}
<QuickSummary />
{/* Se transmite por streaming cuando HeavyStats termina de obtener datos */}
<Suspense fallback={<div>Cargando estadísticas...</div>}>
<HeavyStats />
</Suspense>
</div>
);
}
// app/dashboard/components/HeavyStats.tsx (Server Component)
async function HeavyStats() {
// Aunque esto tarde 2 segundos, el resto de la página no se bloquea
const stats = await fetchExpensiveStats();
return (
<div>
<p>Ingresos: {stats.revenue}</p>
<p>Usuarios: {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.
FAQ
¿Puedo usar useState o useEffect en un Server Component?
No. Los Server Components se ejecutan solo en el servidor, por lo que los hooks que dependen del estado del cliente (useState, useReducer, useEffect, useRef) no están disponibles. Si necesitas estos hooks, marca el componente con "use client". Un patrón común es mantener el padre que obtiene datos como Server Component y mover la parte interactiva a un pequeño Client Component hijo.
¿Los Server Components reemplazan a SSR?
Se complementan mutuamente. SSR genera HTML en el servidor y luego hidrata el mismo componente en el cliente — el bundle JS sigue enviándose. Los Server Components se ejecutan solo en el servidor y no envían JS. En Next.js App Router, ambos trabajan juntos: los Server Components transmiten su salida por streaming y los Client Components se hidratan normalmente.
¿Cómo paso datos de un Server Component a un Client Component?
A través de props — de la misma forma que pasas datos entre cualquier componente React. La restricción clave es que las props deben ser serializables (strings, números, objetos planos, arrays, etc.). No puedes pasar funciones, instancias de clase o nodos DOM como props a un Client Component.
¿Puede un Client Component importar un Server Component?
No directamente. Si un Client Component intenta hacer import de un Server Component, el módulo importado se convierte en parte del bundle del cliente. En su lugar, usa el patrón children: acepta un prop children (o cualquier ReactNode) en el Client Component y pasa el Server Component desde la capa de Server Component.
¿Funcionan los Server Components sin Next.js?
RSC es una característica de React, no de Next.js. Sin embargo, requiere un bundler y runtime de servidor compatibles. Actualmente, Next.js App Router es la integración más madura. Otros frameworks como Remix y Waku también están adoptando RSC, pero Next.js sigue siendo la opción principal lista para producción.
¿Qué pasa con las librerías npm importadas en Server Components?
Se ejecutan en el servidor y su código no se incluye en el bundle JavaScript del cliente. Esto significa que puedes importar librerías grandes — parsers de markdown, formateo de fechas, procesamiento de datos — en Server Components sin afectar el tamaño del bundle que descargan tus usuarios.
¿Cuándo debo usar Server Actions vs rutas API?
Los Server Actions son ideales para envíos de formularios y mutaciones de datos porque se integran directamente con el manejo de formularios de React y la revalidación de caché. Las rutas API (Route Handlers) son mejores cuando necesitas un endpoint API público, receptor de webhooks, o cuando clientes que no son React necesitan llamar al mismo endpoint. Para una app típica de Next.js, prefiere Server Actions para mutaciones internas.
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.
Artículos relacionados: