Conceptos Básicos de Datos en Next.js
12 ejemplos para comenzar con Next.js Data -- 8 básicos y 4 intermedios.
Requisitos previos
Todos los ejemplos asumen un proyecto Next.js 15+ con App Router y TypeScript. Si aún no tienes uno:
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
npm run devTres convenciones aplican a cada ejemplo a continuación:
- Los archivos bajo
app/se ejecutan en el servidor por defecto. Añade"use client"solo al inicio si necesitas interactividad, APIs del navegador o estado de React. - Los Server Components pueden ser async --
await fetch(...)directamente en el cuerpo del componente. - Los Server Actions son funciones etiquetadas con
"use server", invocables desde Client Components y<form action={...}>.
Ejemplos Básicos
1. Fetch en un Server Component
Llama fetch directamente dentro de un Server Component async -- sin useEffect, sin hooks de carga.
// app/posts/page.tsx
interface Post {
id: number;
title: string;
}
export default async function PostsPage() {
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>
);
}- Los Server Components pueden ser funciones async --
awaitfunciona como lo hace en un handler Node regular. - Next.js deduplica llamadas
fetchidénticas dentro de una única solicitud, así que puedes llamar el mismo endpoint desde múltiples componentes. - Los datos nunca se envían al cliente -- solo se envía el HTML renderizado.
- Para Client Components, obtén datos mediante SWR o TanStack Query; nunca mediante
useEffectcomo predeterminado.
Relacionado: Data Fetching -- patrones, deduplicación, manejo de errores | Server Components -- qué se ejecuta dónde
2. Fetches en Paralelo con Promise.all
Dispara solicitudes en paralelo para que la más lenta establezca el tiempo total de espera, no la suma.
// app/dashboard/page.tsx
interface User {
name: string;
}
interface Stats {
total: number;
}
async function getUser(): Promise<User> {
return (await fetch("https://api.example.com/me")).json();
}
async function getStats(): Promise<Stats> {
return (await fetch("https://api.example.com/stats")).json();
}
export default async function Dashboard() {
const [user, stats] = await Promise.all([getUser(), getStats()]);
return (
<h1>
{user.name} -- {stats.total} items
</h1>
);
}- Las llamadas
awaitsecuenciales crean una cascada -- cada solicitud espera a que termine la anterior. Promise.allinicia todas las solicitudes a la vez; el tiempo total es la solicitud más lenta.- Usa
Promise.allSettledcuando un fallo no debe rechazar todo el conjunto. - Para trabajo en paralelo en todo el árbol, prefiere límites de Suspense para que los paneles rápidos se transmitan primero.
Relacionado: Parallel Promises --
Promise.all,allSettledy eliminación de cascadas | Streaming -- fetches en paralelo con Suspense por panel
3. Server Action para Mutación
Define una función solo para servidor con "use server" y conéctala directamente a un formulario.
// app/posts/actions.ts
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
await fetch("https://api.example.com/posts", {
method: "POST",
body: JSON.stringify({ title }),
});
}
// app/posts/form.tsx
import { createPost } from "./actions";
export default function PostForm() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Crear</button>
</form>
);
}- Los Server Actions se ejecutan en el servidor -- nunca se envían al cliente, así que secretos y clientes de DB permanecen seguros.
- Conectar uno a
<form action={...}>funciona incluso sin JavaScript -- mejora progresiva gratis. - El argumento
FormDataproviene de las entradas nombradas del formulario automáticamente. - Siempre vuelve a validar autenticación y autorización dentro de la acción -- nunca confíes en quien la llama.
Relacionado: Server Actions (Next.js) -- patrones, errores, redirecciones | Server Actions (React 19) -- la primitiva subyacente | Server Action Forms -- flujo de formulario de extremo a extremo
4. Revalidar una Ruta Después de una Mutación
Invalida el caché de una ruta específica después de escribir datos para que el siguiente renderizado sea fresco.
// app/posts/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
await fetch("https://api.example.com/posts", {
method: "POST",
body: JSON.stringify({ title }),
});
revalidatePath("/posts");
}revalidatePath(path)marca cualquier entrada de caché vinculada a esa ruta como obsoleta -- la siguiente visita vuelve a obtener.- Para control más fino en rutas, usa
revalidateTag("posts")y etiqueta tus llamadasfetchconnext: { tags: ["posts"] }. - ISR basado en tiempo usa
next: { revalidate: 60 }en elfetchmismo -- elige segundos según sea necesario. - No llames
revalidatePathdesde el cuerpo de renderizado de un Server Component -- solo desde Server Actions o Route Handlers.
Relacionado: Revalidation --
revalidatePath,revalidateTag, ISR | Caching -- qué se almacena en caché y por cuánto tiempo
5. Transmitir una Ruta con loading.tsx
Coloca un archivo loading.tsx para mostrar un fallback mientras el Server Component de la ruta se transmite.
// app/posts/loading.tsx
export default function Loading() {
return <p>Cargando posts...</p>;
}
// app/posts/page.tsx
interface Post {
id: number;
title: string;
}
export default async function PostsPage() {
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>
);
}loading.tsxes un archivo convencional -- Next envuelve automáticamente elpage.tsxhermano en un límite de Suspense.- El fallback se transmite al navegador inmediatamente; el contenido real lo reemplaza cuando está listo.
- Usa skeletons que coincidan con el layout final para evitar cambios de layout cuando llega el contenido.
- Para carga más granular (un panel lento, no la ruta completa), usa límites
<Suspense>explícitos en su lugar.
Relacionado: Streaming & Suspense -- loading.tsx, Suspense, skeletons | Suspense patterns -- cuándo y cómo dividir límites
6. Opciones de Caché de fetch
Next.js extiende fetch con controles de caché -- elige el que coincida con qué tan fresco necesita ser el dato.
// Completamente cacheado en solicitudes (opt-in en Next 15)
const cached = await fetch("https://api.example.com/data", {
cache: "force-cache",
});
// Siempre fresco, nunca cacheado
const fresh = await fetch("https://api.example.com/data", {
cache: "no-store",
});
// Regenerar en el fondo cada 60 segundos (ISR)
const isr = await fetch("https://api.example.com/data", {
next: { revalidate: 60 },
});
// Etiquetar para revalidación bajo demanda
const tagged = await fetch("https://api.example.com/data", {
next: { tags: ["posts"] },
});- En Next.js 15, el predeterminado es
"no-store"-- debes opt-in al almacenamiento en caché, a diferencia de Next 14. cache: "force-cache"almacena en caché indefinidamente; empareja conrevalidateTagpara invalidar bajo demanda.next.revalidateestablece una expiración basada en tiempo en segundos -- bueno para datos que pueden estar ligeramente obsoletos.next.tagspermite invalidar un grupo de fetches con una llamadarevalidateTagdesde una acción.
Relacionado: Caching -- las cuatro capas de caché en profundidad | Revalidation -- invalidación bajo demanda
7. Leer searchParams en una Página Server
Las páginas reciben parámetros de consulta URL como prop async -- sin hook de router requerido.
// app/search/page.tsx
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; page?: string }>;
}) {
const { q = "", page = "1" } = await searchParams;
const res = await fetch(
`https://api.example.com/search?q=${q}&page=${page}`
);
const results = await res.json();
return <pre>{JSON.stringify(results, null, 2)}</pre>;
}- En Next.js 15,
searchParamses una Promise -- siempre espérala antes de leer claves. - Los valores son siempre
string | string[] | undefined-- analiza números y booleanos tú mismo. - Para Client Components, usa
useSearchParams()denext/navigationen su lugar. - Cambiar
searchParamsvuelve a ejecutar el Server Component -- no se necesita refetch explícito.
Relacionado: Search Params -- servidor vs. cliente, establecer parámetros, parseadores tipados | Navigation -- actualizar la URL desde links y acciones
8. cookies() y headers()
Lee encabezados de solicitud y cookies en el servidor con las APIs async de next/headers.
// app/me/page.tsx
import { cookies, headers } from "next/headers";
export default async function MePage() {
const cookieStore = await cookies();
const headerStore = await headers();
const theme = cookieStore.get("theme")?.value ?? "light";
const userAgent = headerStore.get("user-agent");
return (
<p>
Theme: {theme} | UA: {userAgent}
</p>
);
}- Ambas
cookies()yheaders()son async en Next.js 15 -- siempre espéralas. - Leer cualquiera de ellas opta la ruta en renderizado dinámico -- ya no puede ser almacenada en caché estáticamente.
- Establecer cookies solo funciona desde un Server Action o un Route Handler, no desde el renderizado de un Server Component.
- Usa
headers()para metadatos de solicitud (UA, IP víax-forwarded-for, locale); no mutés encabezados desde aquí.
Relacionado: Cookies & Headers -- lectura, establecimiento, patrones de auth | Server Actions -- dónde mutar cookies
Ejemplos Intermedios
9. Streaming Por Panel con Suspense
Envuelve sub-árboles lentos en <Suspense> para que las partes rápidas se rendericen primero y las partes lentas se transmitan.
// app/dashboard/page.tsx
import { Suspense } from "react";
async function FastPanel() {
const data = await fetch("https://api.example.com/fast").then((r) =>
r.json()
);
return <p>Fast: {data.msg}</p>;
}
async function SlowPanel() {
const data = await fetch("https://api.example.com/slow").then((r) =>
r.json()
);
return <p>Slow: {data.msg}</p>;
}
export default function Dashboard() {
return (
<div>
<Suspense fallback={<p>Cargando panel rápido...</p>}>
<FastPanel />
</Suspense>
<Suspense fallback={<p>Cargando panel lento...</p>}>
<SlowPanel />
</Suspense>
</div>
);
}- Cada límite
<Suspense>se transmite independientemente -- el panel rápido no espera al lento. - El
Dashboardexterior es una función sincrónica; el trabajo async vive en sus hijos. - Combina con
Promise.alldentro de un límite cuando varios fetches pertenecen al mismo panel. - Evita un único límite grande para toda una ruta -- el usuario espera a la hoja más lenta.
Relacionado: Streaming -- posicionamiento de límites, skeletons, errores | Parallel Promises -- agrupación de fetches dentro de un límite
10. Async Generator para una API Paginada
Consume un endpoint paginado perezosamente con async function* y for await -- sin rastreo manual de página en quien llama.
// app/posts/all/page.tsx
interface Post {
id: number;
title: string;
}
async function* paginatedPosts(): AsyncGenerator<Post> {
let page = 1;
while (true) {
const res = await fetch(
`https://api.example.com/posts?page=${page}`
);
const { items, hasMore } = (await res.json()) as {
items: Post[];
hasMore: boolean;
};
for (const item of items) yield item;
if (!hasMore) return;
page++;
}
}
export default async function AllPostsPage() {
const all: Post[] = [];
for await (const post of paginatedPosts()) {
all.push(post);
if (all.length >= 100) break;
}
return <p>{all.length} posts cargados</p>;
}async function*cede valores uno a la vez -- quien llama conduce cuándo extraer la siguiente página.for await (... of ...)consume el generador hasta que retorna o el loop se rompe.- Funciona muy bien para APIs que devuelven un cursor o bandera
hasMore, o para transmitir conjuntos de datos grandes. - Detente temprano con
breakpara limitar el uso de memoria -- el generador simplemente deja de ser extraído.
Relacionado: Async Generators -- patrones, cancelación, Route Handlers | Streaming -- emparejamiento con Suspense para UI progresiva
11. Fetch en el Cliente con SWR
Cuando necesitas actualizaciones en tiempo real, refetches iniciados por el usuario, o desplazamiento infinito, obtén datos desde un Client Component con SWR.
"use client";
import useSWR from "swr";
interface User {
id: string;
name: string;
}
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export default function UserCard({ id }: { id: string }) {
const { data, error, isLoading } = useSWR<User>(`/api/users/${id}`, fetcher);
if (isLoading) return <p>Cargando...</p>;
if (error) return <p>Fallo al cargar</p>;
if (!data) return null;
return <h2>{data.name}</h2>;
}- SWR te da stale-while-revalidate: datos instantáneos del caché, luego actualización en fondo.
- Centraliza el
fetcheren una utilidad para que encabezados de auth y manejo de errores se mantengan consistentes. - Usa
mutatede SWR para actualizar optimistamente el caché antes de que el servidor confirme. - Por defecto usa Server Components para obtención de datos -- alcanza SWR solo cuando el cliente genuinamente lo necesita.
Relacionado: SWR Fetch Utility -- cliente centralizado, auth, manejo de errores | SWR Basic Fetching -- claves, fetchers, configuración
12. Server Action con useActionState
Combina una server action, useActionState y revalidatePath para un formulario que maneja estado pendiente, errores de validación e invalidación de caché en un flujo.
// app/contact/actions.ts
"use server";
import { revalidatePath } from "next/cache";
type State = { ok: boolean; message: string };
export async function submitContact(
_prev: State | null,
formData: FormData
): Promise<State> {
const email = formData.get("email") as string;
if (!email.includes("@")) {
return { ok: false, message: "Email inválido" };
}
await fetch("https://api.example.com/contacts", {
method: "POST",
body: JSON.stringify({ email }),
});
revalidatePath("/contact");
return { ok: true, message: "¡Gracias!" };
}
// app/contact/form.tsx
"use client";
import { useActionState } from "react";
import { submitContact } from "./actions";
export default function ContactForm() {
const [state, action, isPending] = useActionState(submitContact, null);
return (
<form action={action}>
<input name="email" type="email" />
<button type="submit" disabled={isPending}>
{isPending ? "Enviando..." : "Enviar"}
</button>
{state && <p>{state.message}</p>}
</form>
);
}useActionState(action, initialState)retorna[state, action, isPending]-- conectaactiona<form action={...}>.- El valor retornado de la acción se convierte en el siguiente
state-- úsalo para errores de validación y mensajes de éxito. revalidatePathinvalida la ruta cacheada para que el siguiente renderizado refleje los nuevos datos.- Deshabilita el botón de submit en
isPendingpara prevenir dobles envíos y reintentos accidentales.
Relacionado: Server Actions -- patrones completos de acción y manejo de errores | useActionState -- la API del hook | Server Action Forms -- flujos de formulario completos