React Server Components: qué son, por qué importan y cuándo usarlos
February 19, 2026
El cambio más grande en React desde los hooks
Cuando React introdujo los hooks en 2019, cambió la forma en que se escribe lógica de estado. Los React Server Components (RSC), estables desde React 19, cambian algo más fundamental: dónde se ejecuta el código.
Este cambio tiene consecuencias profundas en performance, seguridad y experiencia del desarrollador. Y sin embargo, muchos equipos todavía los usan de la misma forma que usaban los componentes de siempre, sin aprovechar lo que los hace diferentes.
El modelo mental correcto
Antes de RSC, React tenía un solo ambiente de ejecución: el navegador. Los Server Components agregan un segundo: el servidor.
Antes (pre-RSC)
─────────────────────────────────────
Servidor: envía HTML inicial (SSR) o nada (SPA)
Cliente: React hidrata y toma el control de todo
Con RSC
─────────────────────────────────────
Servidor: renderiza Server Components → envía HTML + RSC payload
Cliente: React solo hidrata los Client Components
La clave: los Server Components nunca se envían como JavaScript al navegador. Solo su output HTML llega al cliente. Eso significa que sus imports, su lógica, y las librerías que usan no aumentan el bundle del cliente.
Diferencias en la práctica
Server Components (por defecto en Next.js App Router)
// app/products/page.tsx
// ✅ Este archivo NO agrega JS al cliente
import { db } from "@/lib/database"; // librería de servidor, 0 KB al cliente
import ProductCard from "./ProductCard";
export default async function ProductsPage() {
// fetch directo, sin useEffect, sin loading state
const products = await db.query("SELECT * FROM products");
return (
<div className="grid grid-cols-3 gap-6">
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}
Lo que hace este componente:
- Consulta la base de datos directamente (sin API route)
- Renderiza el HTML en el servidor
- Envía al cliente solo HTML, sin JS de React para este componente
- No puede tener
useState,useEffectni event handlers
Client Components (opt-in con "use client")
// components/AddToCartButton.tsx
"use client"; // ← esta directiva lo hace un Client Component
import { useState } from "react";
export default function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return (
<button
onClick={() => {
addToCart(productId);
setAdded(true);
}}
className={added ? "bg-green-500" : "bg-blue-500"}
>
{added ? "✓ Agregado" : "Agregar al carrito"}
</button>
);
}
Este componente necesita ser Client Component porque:
- Usa
useState(estado local) - Tiene un event handler (
onClick) - Su UI cambia en respuesta a interacciones del usuario
La regla de composición
La parte más confusa de RSC: un Server Component puede renderizar Client Components, pero no al revés.
// ✅ Válido: Server Component renderiza Client Component
// app/page.tsx (Server Component)
import InteractiveWidget from "./InteractiveWidget"; // Client Component
export default function Page() {
return (
<main>
<h1>Título renderizado en el servidor</h1>
<InteractiveWidget /> {/* Se hidrata en el cliente */}
</main>
);
}
// ❌ Error silencioso: importar Server Component dentro de Client Component
"use client";
// Esto convierte implícitamente ServerOnlyComponent en Client Component,
// eliminando todos sus beneficios
import ServerOnlyComponent from "./ServerOnlyComponent";
El patrón correcto cuando necesitás pasar Server Components como hijos de Client Components es usando la prop children:
// ClientWrapper.tsx
"use client";
export default function ClientWrapper({
children,
}: {
children: React.ReactNode;
}) {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(!open)}>Toggle</button>
{open && children} {/* children puede ser un Server Component */}
</div>
);
}
// page.tsx (Server Component)
import ClientWrapper from "./ClientWrapper";
import ServerContent from "./ServerContent"; // Server Component
export default function Page() {
return (
<ClientWrapper>
<ServerContent /> {/* ✅ Se renderiza en el servidor */}
</ClientWrapper>
);
}
Cuándo usar cada uno
Una heurística práctica:
| Necesitás... | Tipo de componente |
| --------------------------------------------- | ------------------ |
| Acceder a base de datos o filesystem | Server Component |
| Usar variables de entorno privadas | Server Component |
| Importar librerías pesadas (solo para render) | Server Component |
| useState / useReducer | Client Component |
| useEffect | Client Component |
| Event handlers (onClick, onChange) | Client Component |
| Browser APIs (localStorage, geolocation) | Client Component |
| Animaciones con estado | Client Component |
La regla de oro: empezá siempre con Server Component. Agregá
"use client"solo cuando la funcionalidad lo requiere.
El impacto real en performance
Tomemos un ejemplo concreto: una página de blog que muestra posts desde una base de datos.
Antes (Client Component con useEffect):
"use client";
export default function BlogPage() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/posts")
.then((r) => r.json())
.then((data) => {
setPosts(data);
setLoading(false);
});
}, []);
if (loading) return <Spinner />;
return <PostList posts={posts} />;
}
Problemas:
- El usuario ve un spinner hasta que llega la respuesta
- El JS de este componente viaja al cliente (hidratación)
- Hay un round-trip extra: cliente → API → DB → API → cliente
Con Server Component:
// No "use client", no hooks, no spinner
import { getPosts } from "@/lib/content/posts";
export default async function BlogPage() {
const posts = await getPosts(); // directo, sin API route
return <PostList posts={posts} />;
}
Resultado: el HTML llega al cliente ya con los posts. Zero spinners, zero round-trips adicionales, zero JS del componente en el bundle.
Lo que RSC no resuelve
Para ser justo: RSC no es la solución a todo.
- Personalización en tiempo real: si el contenido cambia según el usuario autenticado y necesitás actualizaciones inmediatas, vas a necesitar Client Components con fetch o SWR/React Query.
- Componentes altamente interactivos: formularios complejos, drag & drop, interfaces ricas en estado.
- Librerías de terceros que no son RSC-compatible: muchas librerías de UI asumen que están en el cliente. Hay que wrapearlas con
"use client".
El patrón que más vale la pena adoptar
Si tuviese que resumir la estrategia en una línea:
Bajá los datos lo más cerca posible de donde se renderizan, usá Server Components para fetch y render, y reservá Client Components solo para interactividad real.
Este patrón reduce el bundle del cliente, elimina waterfalls de fetch, simplifica el manejo de loading states y hace el código más fácil de entender porque cada componente tiene exactamente un trabajo.
En todos los sitios que construimos en Logos, el App Router de Next.js con Server Components es la base. No porque sea la tecnología más nueva, sino porque produce sitios más rápidos y más fáciles de mantener. Eso beneficia directamente al cliente final.