Obtención de datos en Componentes de Servidor
Obtén datos directamente dentro de Componentes de Servidor asincronos con almacenamiento en caché y deduplicación incorporados.
Receta
Tarjeta de referencia rápida -- lista para copiar y pegar.
// app/posts/page.tsx (Server Component -- el predeterminado)
export default async function PostsPage() {
// Almacenado en caché por defecto (force-cache)
const res = await fetch("https://api.example.com/posts");
const posts: Post[] = await res.json();
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
// Revalidación basada en tiempo
await fetch(url, { next: { revalidate: 60 } });
// Sin almacenamiento en caché -- siempre fresco
await fetch(url, { cache: "no-store" });
// Revalidación basada en etiquetas
await fetch(url, { next: { tags: ["posts"] } });Cuándo usarlo: Necesitas cargar datos para una página o componente que no tiene interactividad -- sin useState, sin onClick, sin APIs del navegador.
Ejemplo Funcional
// lib/api.ts
export type Post = { id: number; title: string; body: string };
export async function getPosts(): Promise<Post[]> {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
next: { revalidate: 300, tags: ["posts"] },
});
if (!res.ok) throw new Error("Failed to fetch posts");
return res.json();
}
export async function getPost(id: number): Promise<Post> {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`,
{ next: { tags: [`post-${id}`] } }
);
if (!res.ok) throw new Error(`Post ${id} not found`);
return res.json();
}// app/posts/page.tsx
import { getPosts } from "@/lib/api";
import Link from "next/link";
export default async function PostsPage() {
const posts = await getPosts();
return (
<main className="max-w-2xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-4">Posts</h1>
<ul className="space-y-2">
{posts.slice(0, 10).map((post) => (
<li key={post.id}>
<Link
href={`/posts/${post.id}`}
className="text-blue-600 hover:underline"
>
{post.title}
</Link>
</li>
))}
</ul>
</main>
);
}// app/posts/[id]/page.tsx
import { getPost } from "@/lib/api";
import { notFound } from "next/navigation";
type Props = { params: Promise<{ id: string }> };
export default async function PostPage({ params }: Props) {
const { id } = await params;
const numericId = Number(id);
if (Number.isNaN(numericId)) notFound();
const post = await getPost(numericId);
return (
<article className="max-w-2xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-2">{post.title}</h1>
<p className="text-gray-700 leading-relaxed">{post.body}</p>
</article>
);
}Lo que esto demuestra:
- Obtención de datos directamente en Componentes de Servidor asincronos -- sin
useEffect, sin estado de carga del lado del cliente - Centralizar la lógica de fetch en un archivo
lib/para reutilización entre componentes - Usar
next.revalidatepara ISR basado en tiempo ynext.tagspara revalidación bajo demanda - Acceso a
paramsde ruta dinámica como una Promise (patrón de Next.js 15+)
Análisis Profundo
Cómo Funciona
- En el App Router, cada componente es un Componente de Servidor por defecto. Los Componentes de Servidor pueden ser
asyncy llamarawait fetch()directamente en el cuerpo de la función. - Next.js extiende la API nativa
fetchcon opcionescacheynext. El comportamiento predeterminado en Next.js 15+ escache: "auto", que permite que el framework decida según el contexto. - Cuando la misma URL se obtiene varias veces durante un único pase de renderizado del servidor, React deduplica las solicitudes automáticamente mediante memoización de solicitud -- solo se realiza una llamada de red.
- Los datos obtenidos pueden almacenarse en el Data Cache, que persiste entre solicitudes y despliegues hasta ser revalidados.
- Los errores lanzados durante la obtención pueden ser capturados por el límite
error.tsxmás cercano.
Variaciones
Obtención de datos en paralelo (evitar cascadas):
export default async function DashboardPage() {
// Inicia ambos fetches simultáneamente
const [users, orders] = await Promise.all([
fetch("https://api.example.com/users").then((r) => r.json()),
fetch("https://api.example.com/orders").then((r) => r.json()),
]);
return (
<>
<UserTable users={users} />
<OrderList orders={orders} />
</>
);
}Fuentes de datos que no son fetch (base de datos, ORM):
import { cache } from "react";
import { db } from "@/lib/db";
// Envuelve con React.cache para memoización a nivel de solicitud
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});Pasar una promesa a un Componente de Cliente:
// Componente de Servidor
export default async function Page() {
const dataPromise = fetchSlowData(); // NO hagas await
return <ClientChart dataPromise={dataPromise} />;
}Notas de TypeScript
// Siempre tipifica tus respuestas de fetch
type ApiResponse<T> = { data: T; total: number };
async function getItems(): Promise<ApiResponse<Item[]>> {
const res = await fetch("/api/items");
return res.json();
}
// params es una Promise en Next.js 15+
type PageProps = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};Problemas Comunes
-
Cascadas de fetch -- Las llamadas
awaitsecuenciales crean cascadas donde cada fetch espera al anterior. Solución: UsaPromise.all()para solicitudes independientes o mueve cada fetch a su propio componente envuelto con Suspense para streaming en paralelo. -
Olvidar manejo de errores -- Un error de fetch no detectado bloquea toda la ruta. Solución: Lanza un error desde tu asistente de fetch para que el límite
error.tsxmás cercano lo capture, o usa try/catch para manejo granular. -
cache: "no-store"hace que toda la ruta sea dinámica -- Si cualquier fetch en una ruta usano-store, toda la ruta optar out de la generación estática. Solución: Aísla los fetches dinámicos en componentes separados envueltos conSuspensepara que el resto de la ruta pueda permanecer estático. -
Las llamadas sin fetch no se deduplicarán automáticamente -- Las consultas directas a bases de datos o llamadas de SDK de terceros omiten la memoización de solicitud de React. Solución: Envuélvelas con
React.cache()para obtener deduplicación por solicitud. -
params y searchParams son Promises en Next.js 15+ -- Desestructurarlos directamente sin
awaitte da un objeto Promise, no los valores. Solución: Siempreawait paramsyawait searchParamsantes de acceder a las propiedades.
Alternativas
| Alternativa | Úsalo Cuando | No lo uses Cuando |
|---|---|---|
| Route Handlers (rutas API) | Necesitas un endpoint REST independiente para consumidores externos | Solo necesitas datos dentro de un Componente de Servidor |
| SWR o React Query en el cliente | Necesitas actualizaciones en tiempo real, polling, o mutaciones optimistas | Los datos pueden obtenerse una vez en el servidor |
| Server Actions | Necesitas mutar datos, no leerlos | Solo necesitas solicitudes GET |
React.cache + ORM | Obttienes datos de una base de datos, no de una API HTTP | Estás llamando a una API REST pública |
unstable_cache (Next.js) | Necesitas semántica de Data Cache para fuentes de datos que no son fetch | Plain fetch con next.revalidate ya funciona |
Preguntas Frecuentes
¿Por qué los Componentes de Servidor pueden ser asincronos pero los Componentes de Cliente no?
- Los Componentes de Servidor se ejecutan en el servidor donde async/await es nativamente soportado
- Los Componentes de Cliente se renderizarán en el navegador donde React no soporta componentes de función asincronos
- Usa
useEffecto librerías como SWR para obtención asincronida de datos en Componentes de Cliente
¿Cuál es la diferencia entre cache: "no-store" y next: { revalidate: 0 }?
- Ambas optan la ruta en renderizado dinámico
cache: "no-store"omite completamente el Data Cacherevalidate: 0también omite almacenamiento en caché pero señala intención a través de la API de ISR- En la práctica se comportan igual en Next.js 15+
¿Cómo funciona la memoización de solicitud de React con fetch?
- Cuando la misma URL y opciones se obtienen varias veces durante un único renderizado del servidor, React las deduplica automáticamente
- Solo se realiza una llamada de red; todos los llamadores reciben el mismo resultado
- La memoización se borra después de que se completa el renderizado -- no persiste entre solicitudes
¿Cuándo debes usar React.cache() en lugar de confiar en deduplicación automática de fetch?
- Usa
React.cache()para fuentes de datos que no son fetch como consultas directas a bases de datos o llamadas ORM - La deduplicación automática solo se aplica a la API
fetch React.cache()proporciona memoización por solicitud para cualquier función asincronía
¿Cómo tipificar la prop params en un componente de página de ruta dinámica?
// Next.js 15+: params es una Promise
type Props = { params: Promise<{ id: string }> };
export default async function Page({ params }: Props) {
const { id } = await params;
// usa id
}¿Qué sucede si desestructuras params sin esperar en Next.js 15+?
- Obtienes un objeto Promise en lugar de los valores reales
- Tu código fallará silenciosamente o producirá resultados inesperados como
"[object Promise]" - Siempre
await paramsantes de acceder a las propiedades
¿Cómo evitas cascadas de fetch cuando tienes múltiples obtenciones de datos independientes?
// Usa Promise.all para ejecutar fetches en paralelo
const [users, orders] = await Promise.all([
fetch("/api/users").then((r) => r.json()),
fetch("/api/orders").then((r) => r.json()),
]);¿Por qué usar cache: "no-store" en un fetch hace que toda la ruta sea dinámica?
- Next.js detecta que la ruta depende de datos a tiempo de solicitud
- Un único fetch dinámico evita que toda la ruta se genere estáticamente
- Solución: aísla el fetch dinámico en un componente separado envuelto con
<Suspense>
¿Cómo debes tipificar una respuesta de fetch para garantizar seguridad de tipos?
type ApiResponse<T> = { data: T; total: number };
async function getItems(): Promise<ApiResponse<Item[]>> {
const res = await fetch("/api/items");
if (!res.ok) throw new Error("Failed to fetch");
return res.json();
}¿Cuál es el comportamiento de almacenamiento en caché predeterminado de fetch en Next.js 15+?
- El predeterminado es
cache: "auto", que permite que el framework decida según el contexto - Esto difiere de Next.js 14 donde el predeterminado era
force-cache - Sé explícito estableciendo
cacheonext.revalidateen cada llamada de fetch
¿Cómo pasas datos obtenidos de un Componente de Servidor a un Componente de Cliente sin esperar?
// Componente de Servidor: pasa la promesa, no el valor resuelto
export default async function Page() {
const dataPromise = fetchSlowData(); // NO hagas await
return <ClientChart dataPromise={dataPromise} />;
}- El Componente de Cliente puede consumir la promesa usando el hook
use()de React
¿Qué sucede si una llamada de fetch lanza un error y no hay manejo de errores?
- El error bloquea toda la ruta
- El límite
error.tsxmás cercano lo captura si existe uno - Siempre lanza desde asistentes de fetch o usa try/catch para manejo granular
Relacionado
- Server Actions -- Mutación de datos desde el servidor
- Revalidation -- Refrescando datos almacenados en caché bajo demanda
- Caching -- Entendiendo las cuatro capas de almacenamiento en caché
- Streaming -- Cargando datos progresivamente con Suspense