Componentes del Servidor
Renderiza componentes React en el servidor con cero JavaScript del lado del cliente -- el comportamiento predeterminado en el Next.js App Router.
Receta
Tarjeta de referencia rápida -- lista para copiar y pegar.
// app/page.tsx -- Server Component de forma predeterminada (no se necesita directiva)
import { db } from "@/lib/db";
export default async function HomePage() {
const posts = await db.post.findMany({ take: 10 });
return (
<main>
<h1>Últimas Publicaciones</h1>
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
</main>
);
}Cuándo usarlo: Cualquier componente que solo lea datos y renderice markup -- sin useState, sin useEffect, sin event handlers, sin browser APIs. Este es el comportamiento predeterminado; tu opt out es con "use client", no tu opt in.
Ejemplo Funcional
// app/blog/page.tsx (Server Component)
import { Suspense } from "react";
import { formatDistanceToNow } from "date-fns";
type Post = {
id: string;
title: string;
excerpt: string;
publishedAt: string;
author: { name: string; avatar: string };
};
async function fetchPosts(): Promise<Post[]> {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 300 },
});
if (!res.ok) throw new Error("Error al obtener publicaciones");
return res.json();
}
export default async function BlogPage() {
return (
<main className="max-w-3xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-8">Blog</h1>
<Suspense fallback={<PostsSkeleton />}>
<PostList />
</Suspense>
</main>
);
}
async function PostList() {
const posts = await fetchPosts();
return (
<div className="space-y-8">
{posts.map((post) => (
<article key={post.id} className="border-b pb-6">
<h2 className="text-xl font-semibold mb-2">{post.title}</h2>
<p className="text-gray-600 mb-3">{post.excerpt}</p>
<div className="flex items-center gap-3 text-sm text-gray-500">
<img
src={post.author.avatar}
alt={post.author.name}
className="w-6 h-6 rounded-full"
/>
<span>{post.author.name}</span>
<span>
{formatDistanceToNow(new Date(post.publishedAt), {
addSuffix: true,
})}
</span>
</div>
</article>
))}
</div>
);
}
function PostsSkeleton() {
return (
<div className="space-y-8">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="border-b pb-6">
<div className="h-6 bg-gray-200 rounded w-3/4 mb-2 animate-pulse" />
<div className="h-4 bg-gray-100 rounded w-full mb-3 animate-pulse" />
<div className="h-4 bg-gray-100 rounded w-1/2 animate-pulse" />
</div>
))}
</div>
);
}Lo que esto demuestra:
- Server Component async obteniendo datos con
awaitdirectamente en el cuerpo de la función - Usando una biblioteca pesada (
date-fns) que envía cero JavaScript al cliente - Separando la búsqueda de datos async en un componente hijo envuelto en
<Suspense>para streaming - Sin directiva
"use client"en ningún lado -- toda la página es renderizada en el servidor
Análisis Profundo
Cómo Funciona
- Los componentes son Server Components de forma predeterminada en el App Router. No se necesita directiva.
- Los Server Components se ejecutan en el servidor y producen un payload RSC -- un árbol React serializado y compacto que se transmite al cliente.
- Los Server Components pueden ser
asyncy usarawaitdirectamente. Esto no está permitido en Client Components. - Pueden acceder a bases de datos, sistemas de archivos, variables de entorno y cualquier recurso solo del servidor directamente.
- Las dependencias usadas solo en Server Components (ej. analizadores markdown, resaltadores de sintaxis,
date-fns) no se incluyen en el bundle del cliente. - Los Server Components no se vuelven a renderizar en el cliente en respuesta a cambios de estado. Solo se vuelven a ejecutar cuando la ruta cambia o se llama a
revalidatePath/revalidateTag. - El payload RSC es reconciliado con el DOM del lado del cliente por React, preservando el estado del Client Component durante las navegaciones.
Variaciones
Obtención de datos en paralelo:
async function Dashboard() {
const [users, revenue, orders] = await Promise.all([
fetchUsers(),
fetchRevenue(),
fetchOrders(),
]);
return (
<>
<UserTable users={users} />
<RevenueChart revenue={revenue} />
<OrderList orders={orders} />
</>
);
}Pasando datos del servidor a Client Components:
// Server Component
import { ClientMap } from "./client-map";
export default async function LocationPage() {
const locations = await db.location.findMany();
// Solo los datos serializables pueden cruzar el límite
return <ClientMap locations={locations} />;
}Utilidades solo del servidor:
// lib/server-only-utils.ts
import "server-only"; // Lanza un error de compilación si se importa en un Client Component
export function getSecretConfig() {
return {
apiKey: process.env.SECRET_API_KEY!,
dbUrl: process.env.DATABASE_URL!,
};
}Notas de TypeScript
// Los Server Components async retornan Promise<JSX.Element>
// TypeScript maneja esto con los tipos de React 19+
async function MyComponent(): Promise<JSX.Element> {
const data = await fetchData();
return <div>{data.name}</div>;
}
// Los props deben ser serializables cuando se pasan a Client Components
type SerializableProps = {
name: string;
count: number;
items: { id: string; label: string }[];
// NO permitido: onClick: () => void
// NO permitido: ref: React.Ref<HTMLDivElement>
};
// Usa el paquete `server-only` para protección en tiempo de compilación
import "server-only";Gotchas
-
No puedes usar hooks --
useState,useEffect,useRefy todos los otros hooks son solo del cliente. Solución: Extrae partes interactivas en un componente"use client". -
No puedes usar event handlers --
onClick,onChange,onSubmit, etc. requieren JavaScript del lado del cliente. Solución: Mueve la lógica del event handler a un Client Component. -
No puedes acceder a browser APIs --
window,document,localStorage,navigatorno están disponibles en el servidor. Solución: Usa estos solo en componentes"use client"o detrás de comprobacionestypeof window !== "undefined". -
Los props a Client Components deben ser serializables -- Las funciones (excepto Server Actions), instancias de clases, Symbols y nodos DOM no pueden pasarse como props cruzando el límite servidor-cliente. Solución: Pasa solo datos simples; usa Server Actions para callbacks.
-
Payloads del servidor grandes -- Obtener demasiados datos en un Server Component y pasar todo como props infla el payload RSC. Solución: Obtén solo lo que el Client Component necesita; pagina en el servidor.
-
Las bibliotecas de terceros pueden no ser compatibles con RSC -- Las bibliotecas que importan
useState,useEffecto browser APIs fallan en Server Components. Solución: Importalas solo dentro de archivos"use client", o usa un componente wrapper.
Alternativas
| Enfoque | Úsalo Cuando | No lo Úses Cuando |
|---|---|---|
| Server Components | Interfaz de solo lectura, obtención de datos, renderizado sin-JS | Interfaz interactiva con estado o efectos |
| Client Components | Interfaz interactiva con hooks, eventos, browser APIs | Visualización de datos puros sin interactividad |
| Renderizado del lado del servidor (SSR) | Aplicaciones pre-RSC heredadas que necesitan HTML del servidor | Tienes acceso al App Router |
| Generación de Sitios Estáticos | El contenido rara vez cambia y puede compilarse en el tiempo de despliegue | Los datos son específicos del usuario o altamente dinámicos |
| Rutas API + client fetch | Los consumidores externos necesitan un endpoint REST | Los datos solo son consumidos por tus propias páginas |
Ejemplo del Mundo Real
De una aplicación SaaS de Next.js 15 / React 19 en producción (SystemsArchitect.io).
// Ejemplo de producción: Página de categoría de FAQ con acceso directo a datos
// Archivo: src/app/faqs/[slug]/page.tsx
export default async function FaqCategoryPage({ params }: FaqCategoryPageProps) {
const { slug } = await params;
const category = await getFaqCategory(slug);
if (!category) {
notFound();
}
const iconConfig = getFaqIcon(category.slug);
const IconComponent = iconConfig.icon;
return (
<div className="min-h-screen bg-zinc-50 dark:bg-black">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<Link href="/faqs" className="inline-flex items-center gap-2 text-sm cursor-pointer">
<ChevronLeft className="h-4 w-4" />
<span>Volver a todas las FAQs</span>
</Link>
<div className="flex items-center gap-3 mb-4">
<div className={iconConfig.color}>
<IconComponent className="h-8 w-8" />
</div>
<h1 className="text-3xl font-bold">{category.title}</h1>
</div>
<FaqList faqs={category.faqs} categorySlug={category.slug} />
</div>
</div>
);
}Lo que esto demuestra en producción:
- Sin directiva "use client", este es un Server Component de forma predeterminada
- La función es
asyncy directamenteawaits datos degetFaqCategory()que llama a Prisma bajo el capó await paramses el patrón Next.js 15+ donde params ahora es una Promise en rutas dinámicasnotFound()de next/navigation dispara el límite not-found.tsx más cercanoFaqList(un Client Component) recibe datos pre-obtenidos como props. El límite servidor-cliente está a nivel de prop- Cero JavaScript del cliente para la lógica de obtención de datos de esta página
FAQs
¿Qué hace que un componente sea un Server Component en Next.js?
Los componentes son Server Components de forma predeterminada en el App Router. No se necesita directiva. Se convierten en Client Components solo cuando añades "use client" al archivo.
¿Pueden los Server Components ser async y usar await?
Sí. Los Server Components pueden ser funciones async y usar await directamente en el cuerpo de la función. Esto no está permitido en Client Components.
¿Los Server Components envían JavaScript al cliente?
No. Los Server Components producen un payload RSC (árbol React serializado) que se transmite al cliente. Las dependencias usadas solo en Server Components (ej. date-fns, analizadores markdown) no se incluyen en el bundle del cliente.
Gotcha: Intenté usar useState en un Server Component y obtuve un error. ¿Por qué?
Los Server Components no pueden usar React hooks (useState, useEffect, useRef, etc.) porque se ejecutan en el servidor y no se vuelven a renderizar en el cliente. Extrae partes interactivas en un componente "use client".
Gotcha: Una biblioteca de terceros falla cuando se importa en un Server Component. ¿Qué hago?
La biblioteca probablemente importa useState, useEffect o browser APIs internamente. Importala solo dentro de un archivo "use client", o crea un componente Client Component wrapper delgado alrededor de ella.
¿Qué datos puedo pasar de un Server Component a un Client Component?
Solo datos serializables: strings, números, booleanos, arrays, objetos simples y Server Actions. No puedes pasar funciones regulares, instancias de clases, Symbols o nodos DOM.
¿Cómo protejo el código solo del servidor de ser importado en un Client Component?
Usa el paquete server-only:
import "server-only";
export function getSecretConfig() {
return { apiKey: process.env.SECRET_API_KEY! };
}Esto lanza un error de compilación si el archivo se importa en un archivo "use client".
¿Cómo obtengo datos en paralelo en un Server Component?
Usa Promise.all para ejecutar múltiples búsquedas concurrentemente:
const [users, revenue, orders] = await Promise.all([
fetchUsers(),
fetchRevenue(),
fetchOrders(),
]);¿Cuándo se vuelven a ejecutar los Server Components?
Los Server Components no se vuelven a renderizar en respuesta a cambios de estado del lado del cliente. Se vuelven a ejecutar solo cuando la ruta cambia o cuando se llama a revalidatePath/revalidateTag.
¿Cuál es el tipo de retorno de TypeScript de un Server Component async?
async function MyComponent(): Promise<JSX.Element> {
const data = await fetchData();
return <div>{data.name}</div>;
}Los tipos de React 19+ manejan Promise<JSX.Element> para componentes async.
¿Cómo escribo props que se pasarán de un Server Component a un Client Component en TypeScript?
Define un tipo con solo campos serializables:
type SerializableProps = {
name: string;
count: number;
items: { id: string; label: string }[];
// NO permitido: onClick: () => void
};¿Qué es el payload RSC y cómo funciona?
- El payload RSC es un árbol React serializado y compacto producido por Server Components en el servidor.
- Se transmite al cliente donde React lo reconcilia con el DOM del lado del cliente.
- El estado del Client Component se preserva durante las navegaciones gracias a esta reconciliación.
Relacionados
- Client Components -- Añadiendo interactividad con
"use client" - Composition Patterns -- Mezclando componentes servidor y cliente
- Static vs Dynamic -- Cómo se determina el modo de renderizado
- Fetching -- Patrones de obtención de datos en Server Components