Los React Server Components (RSC) han cambiado radicalmente la forma en que construimos aplicaciones web con Next.js. En 2026, ya no son una novedad experimental: son el estándar. Este artículo explora en profundidad cómo funcionan, cuándo usarlos y los patrones avanzados que deberías dominar.
¿Qué son los React Server Components?
Los Server Components son componentes de React que se ejecutan exclusivamente en el servidor. A diferencia de los Client Components, nunca envían JavaScript al navegador. Esto significa que puedes acceder directamente a bases de datos, APIs internas y el sistema de archivos sin exponer lógica al cliente.
En Next.js 15 con App Router, todos los componentes son Server Components por defecto. Solo necesitas añadir "use client" cuando requieres interactividad del navegador.
El modelo mental correcto
Piensa en los Server Components como templates que se renderizan en el servidor y envían HTML puro al cliente. El navegador recibe el resultado final, no el código para generarlo.
// app/[locale]/blog/page.tsx — Server Component por defecto
import { getTranslations } from "next-intl/server";
export default async function BlogPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "blog" });
// Acceso directo a datos — sin API intermedia
const posts = await fetch("https://api.example.com/posts", {
next: { revalidate: 3600 },
}).then((res) => res.json());
return (
<main>
<h1>{t("page_title")}</h1>
{posts.map((post: { id: string; title: string }) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</main>
);
}Server Components vs Client Components
La clave está en entender cuándo necesitas cada tipo. No se trata de elegir uno u otro: se trata de componer ambos estratégicamente.
Cuándo usar Server Components
- Fetching de datos: Acceso directo a bases de datos o APIs sin pasar por endpoints públicos.
- Contenido estático o semi-estático: Textos, listados, layouts que no cambian con la interacción del usuario.
- SEO crítico: El contenido se renderiza como HTML completo, ideal para crawlers.
- Componentes pesados: Librerías como syntax highlighters o parsers de Markdown que no necesitan enviarse al cliente.
- Acceso a secretos: Variables de entorno del servidor, tokens de API, credenciales de base de datos.
Cuándo usar Client Components
- Interactividad: onClick, onChange, onSubmit y cualquier event handler.
- Estado local: useState, useReducer, useContext.
- Efectos del navegador: useEffect, IntersectionObserver, localStorage.
- Hooks de terceros: useTranslations (next-intl), useForm (react-hook-form).
// components/common/ThemeToggle.tsx — Client Component
"use client";
import { useState, useCallback } from "react";
export function ThemeToggle() {
const [isDark, setIsDark] = useState(false);
const toggle = useCallback(() => {
setIsDark((prev) => !prev);
document.documentElement.classList.toggle("dark");
}, []);
return (
<button
onClick={toggle}
aria-label={isDark ? "Activar modo claro" : "Activar modo oscuro"}
className="rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-gray-800"
>
{isDark ? "☀️" : "🌙"}
</button>
);
}Patrones de composición avanzados
Patrón contenedor Server + Client
El patrón más poderoso de RSC es pasar Server Components como children de Client Components. Esto te permite tener un wrapper interactivo sin convertir todo el árbol en código cliente.
// components/sections/InteractiveSection.tsx
"use client";
import { useState, type ReactNode } from "react";
interface Props {
title: string;
children: ReactNode;
}
export function InteractiveSection({ title, children }: Props) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<section>
<button
onClick={() => setIsExpanded(!isExpanded)}
aria-expanded={isExpanded}
>
{title}
</button>
{isExpanded && children}
</section>
);
}
// app/[locale]/page.tsx — Server Component
import { InteractiveSection } from "@/components/sections/InteractiveSection";
export default async function Page() {
// Este fetch se ejecuta en el servidor
const data = await fetchHeavyData();
return (
<InteractiveSection title="Ver detalles">
{/* Este contenido se renderiza en el servidor */}
<HeavyDataTable data={data} />
</InteractiveSection>
);
}Streaming con Suspense
Una de las ventajas más potentes de RSC es el streaming. Puedes enviar partes de la página al navegador mientras otras aún se están generando en el servidor.
import { Suspense } from "react";
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
{/* Se muestra inmediatamente */}
<QuickStats />
{/* Se carga con streaming */}
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</main>
);
}
// Cada componente async puede tardar lo que necesite
async function RevenueChart() {
const data = await getRevenueData(); // 2-3 segundos
return <Chart data={data} />;
}
async function RecentOrders() {
const orders = await getRecentOrders(); // 1-2 segundos
return <OrdersTable orders={orders} />;
}Data fetching en Server Components
Fetch con cache y revalidación
Next.js 15 extiende el fetch nativo con opciones de cache que se integran perfectamente con RSC.
// Cache estático con revalidación cada hora
const posts = await fetch("https://api.example.com/posts", {
next: { revalidate: 3600 },
}).then((res) => res.json());
// Sin cache — siempre fresco
const user = await fetch("https://api.example.com/me", {
cache: "no-store",
}).then((res) => res.json());
// Cache estático (por defecto en páginas estáticas)
const config = await fetch("https://api.example.com/config").then((res) =>
res.json()
);Acceso directo a base de datos
Al ejecutarse en el servidor, los RSC pueden acceder directamente a tu ORM o base de datos sin exponer credenciales.
// app/[locale]/projects/page.tsx
import { prisma } from "@/lib/prisma";
export default async function ProjectsPage() {
const projects = await prisma.project.findMany({
where: { published: true },
orderBy: { createdAt: "desc" },
select: {
id: true,
title: true,
description: true,
slug: true,
image: true,
},
});
return (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
);
}Server Actions: mutaciones desde el cliente
Los Server Actions son funciones que se ejecutan en el servidor pero se pueden invocar desde Client Components. Son la forma recomendada de manejar formularios y mutaciones.
// app/actions/contact.ts
"use server";
import { z } from "zod/v4";
const contactSchema = z.object({
name: z.string().min(2),
email: z.email(),
message: z.string().min(10).max(1000),
});
export async function submitContact(formData: FormData) {
const parsed = contactSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
});
if (!parsed.success) {
return { error: "Datos inválidos", issues: parsed.error.issues };
}
// Enviar email, guardar en DB, etc.
await sendEmail(parsed.data);
return { success: true };
}// components/common/ContactForm.tsx
"use client";
import { useActionState } from "react";
import { submitContact } from "@/app/actions/contact";
export function ContactForm() {
const [state, action, isPending] = useActionState(submitContact, null);
return (
<form action={action}>
<input name="name" required aria-label="Nombre" />
<input name="email" type="email" required aria-label="Email" />
<textarea name="message" required aria-label="Mensaje" />
<button type="submit" disabled={isPending}>
{isPending ? "Enviando..." : "Enviar"}
</button>
{state?.error && <p role="alert">{state.error}</p>}
{state?.success && <p role="status">Mensaje enviado correctamente</p>}
</form>
);
}Errores comunes y cómo evitarlos
Usar hooks en Server Components
Este es el error más frecuente. Los hooks de React (useState, useEffect, etc.) solo funcionan en Client Components.
// ❌ Error: hooks en Server Component
export default function Page() {
const [count, setCount] = useState(0); // Error!
return <p>{count}</p>;
}
// ✅ Correcto: extraer la parte interactiva
// components/Counter.tsx
"use client";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// app/page.tsx (Server Component)
import { Counter } from "@/components/Counter";
export default function Page() {
return <Counter />;
}Props no serializables
Al pasar props de Server a Client Components, los datos deben ser serializables. No puedes pasar funciones, Dates, Maps o Sets.
// ❌ Error: función como prop
<ClientComponent onClick={() => console.log("click")} />
// ✅ Correcto: mover la lógica al Client Component
// o usar Server Actions
<ClientComponent actionUrl="/api/action" />Mezclar boundaries incorrectamente
Una vez que un componente es "use client", todos sus imports también serán Client Components. No puedes importar un Server Component desde un Client Component.
// ❌ Error: importar Server Component en Client Component
"use client";
import { ServerOnlyComponent } from "./ServerOnlyComponent"; // Se convierte en Client
// ✅ Correcto: pasar como children
"use client";
export function ClientWrapper({ children }: { children: React.ReactNode }) {
return <div className="interactive">{children}</div>;
}
// En el Server Component padre:
<ClientWrapper>
<ServerOnlyComponent />
</ClientWrapper>Impacto en el rendimiento
Los Server Components reducen drásticamente el JavaScript enviado al cliente. En un proyecto real con Next.js 15, medimos estas mejoras:
- JavaScript bundle: reducción del 40-60% en páginas con mucho contenido estático.
- Time to Interactive (TTI): mejora de 1.5-2 segundos en dispositivos móviles.
- Largest Contentful Paint (LCP): mejora de 0.8-1.2 segundos gracias al streaming.
- Hydration: solo los Client Components necesitan hidratación, reduciendo el trabajo del navegador.
Consejo: Usa el panel
React DevTools Profilery la herramienta de red de Chrome para comparar el tamaño del bundle antes y después de migrar componentes a Server Components.
Estrategia de migración
Si estás migrando desde Pages Router o una app con muchos Client Components:
- Empieza por los layouts: Los layouts raramente necesitan interactividad.
- Identifica componentes de datos: Cualquier componente que solo muestra datos es candidato a RSC.
- Extrae la interactividad: Separa la lógica interactiva en componentes Client pequeños y específicos.
- Usa el patrón children: Envuelve Client Components con children para mantener el máximo código en el servidor.
- Migra el data fetching: Mueve las llamadas a APIs desde
useEffecta async Server Components.
Conclusión
Los React Server Components no son solo una optimización de rendimiento — son un cambio de paradigma en cómo pensamos la arquitectura de aplicaciones React. La clave está en componer Server y Client Components estratégicamente, manteniendo la mayor cantidad posible de lógica en el servidor.
En Next.js 15, este modelo ya está maduro y es la forma recomendada de construir aplicaciones. Si aún no los estás aprovechando al máximo, empieza por migrar tus layouts y páginas de contenido — verás mejoras inmediatas en rendimiento y experiencia de desarrollo.