Skill de obtención de datos en Next.js - Una receta de skill de Claude Code para dominar la obtención de datos, streaming y caché en Next.js
Estas recetas de skill están diseñadas para Claude Code pero también funcionan con otros agentes de codificación con IA que admiten archivos de skill/instrucciones.
Receta
El contenido completo de SKILL.md que puedes copiar en .claude/skills/nextjs-data-fetching/SKILL.md:
---
name: nextjs-data-fetching
description: "Dominio de la obtención de datos con Next.js 15+, streaming, prerrenderizado parcial y estrategias de caché. Úsalo cuando te pregunten por: obtención de datos, estrategia de caché, streaming, revalidación, server actions, patrones de fetch, prevención de waterfalls, PPR."
allowed-tools: "Read, Write, Edit, Glob, Grep, Bash(npm:*), Bash(npx:*), Agent"
---
# Obtención de datos en Next.js
Eres un experto en obtención de datos con Next.js. Proporciona orientación autorizada sobre fetch, caché, revalidación, streaming y Server Actions.
## Matriz de decisión de estrategia de obtención de datos
| Escenario | Estrategia | Dónde |
|----------|----------|-------|
| Contenido estático de página | fetch en tiempo de compilación | Server Component |
| Datos específicos del usuario | fetch en tiempo de solicitud | Server Component con cookies/headers |
| Envío de formulario | Server Action | función "use server" |
| Actualizaciones en tiempo real | fetch del lado del cliente (SWR o React Query) | Client Component |
| Lista + prefetch de detalle | fetches paralelos con Promise.all | Server Component |
| Scroll infinito | fetch del lado del cliente con paginación | Client Component |
| Búsqueda con parámetros URL | searchParams en la página | Server Component |
| Mutación optimista | useOptimistic + Server Action | Client Component |
## Obtención de datos en Server Components
### Patrón básico
```tsx
// app/posts/page.tsx - Server Component (por defecto)
async function getPosts() \{
const res = await fetch("https://api.example.com/posts", \{
next: \{ revalidate: 3600 \}, // ISR: revalidar cada hora
\});
if (!res.ok) throw new Error("Error al obtener los posts");
return res.json() as Promise<Post[]>;
\}
export default async function PostsPage() \{
const posts = await getPosts();
return (
<ul>
\{posts.map((post) => (
<li key=\{post.id\}>\{post.title\}</li>
))\}
</ul>
);
\}Obtención paralela (evitar waterfalls)
// MAL: waterfall secuencial
async function Page() \{
const user = await getUser(); // 200ms
const posts = await getPosts(); // 300ms
// Total: 500ms
// BIEN: obtención paralela
async function Page() \{
const [user, posts] = await Promise.all([
getUser(), // 200ms
getPosts(), // 300ms
]);
// Total: 300ms (el máximo de ambos)Obtención con searchParams
export default async function SearchPage(\{
searchParams,
\}: \{
searchParams: Promise<\{ q?: string; page?: string \}>;
\}) \{
const \{ q, page \} = await searchParams;
const results = await search(q ?? "", Number(page ?? "1"));
return <SearchResults results=\{results\} />;
\}Capas de caché
Next.js 15 cambió el valor por defecto: las solicitudes fetch YA NO se almacenan en caché por defecto.
Memoización de solicitudes
- La misma URL de fetch + opciones en el mismo árbol de renderizado se deduplican automáticamente
- Solo dura durante una única solicitud del servidor
- No se necesita configuración
Data Cache (opt-in en Next.js 15+)
// Opt-in para caché
fetch(url, \{ cache: "force-cache" \});
// Caché con revalidación basada en tiempo
fetch(url, \{ next: \{ revalidate: 3600 \} \});
// Sin caché (por defecto en Next.js 15)
fetch(url, \{ cache: "no-store" \});
// o simplemente: fetch(url) - no-store es el valor por defectoFull Route Cache
- Las rutas estáticas se almacenan en caché por completo en tiempo de compilación
- Las rutas dinámicas (que usan cookies, headers, searchParams) se renderizan por solicitud
- Usa generateStaticParams para la generación estática de rutas dinámicas
Estrategias de revalidación
// Revalidación basada en tiempo
fetch(url, \{ next: \{ revalidate: 60 \} \});
// Revalidación bajo demanda por ruta
import \{ revalidatePath \} from "next/cache";
revalidatePath("/posts");
// Revalidación bajo demanda por tag
import \{ revalidateTag \} from "next/cache";
// Al hacer fetch:
fetch(url, \{ next: \{ tags: ["posts"] \} \});
// Al mutar:
revalidateTag("posts");Caché sin fetch (unstable_cache / cacheTag)
import \{ unstable_cache \} from "next/cache";
const getCachedUser = unstable_cache(
async (id: string) => db.user.findUnique(\{ where: \{ id \} \}),
["user"], // partes de la clave de caché
\{ revalidate: 3600, tags: ["user"] \}
);Streaming con Suspense
import \{ Suspense \} from "react";
export default function DashboardPage() \{
return (
<div>
<h1>Panel</h1>
\{/* Esto se renderiza inmediatamente */\}
<StaticHeader />
\{/* Estos se transmiten de forma independiente */\}
<Suspense fallback=\{<ChartSkeleton />\}>
<RevenueChart />
</Suspense>
<Suspense fallback=\{<TableSkeleton />\}>
<RecentOrders />
</Suspense>
</div>
);
\}
// Cada componente async se transmite cuando está listo
async function RevenueChart() \{
const data = await getRevenue(); // consulta lenta
return <Chart data=\{data\} />;
\}
async function RecentOrders() \{
const orders = await getOrders(); // otra consulta lenta
return <OrdersTable orders=\{orders\} />;
\}Partial Prerendering (PPR)
PPR combina contenido estático y dinámico en una sola ruta:
// next.config.ts
const config = \{
experimental: \{
ppr: true,
\},
\};
// app/product/[id]/page.tsx
import \{ Suspense \} from "react";
export default async function ProductPage(\{
params,
\}: \{
params: Promise<\{ id: string \}>;
\}) \{
const \{ id \} = await params;
const product = await getProduct(id); // estático (en caché)
return (
<div>
\{/* Shell estático - prerrenderizado */\}
<h1>\{product.name\}</h1>
<p>\{product.description\}</p>
\{/* Agujeros dinámicos - transmitidos en tiempo de solicitud */\}
<Suspense fallback=\{<PriceSkeleton />\}>
<DynamicPrice productId=\{id\} />
</Suspense>
<Suspense fallback=\{<ReviewsSkeleton />\}>
<DynamicReviews productId=\{id\} />
</Suspense>
</div>
);
\}Server Actions (mutaciones)
// actions.ts
"use server";
import \{ revalidatePath \} from "next/cache";
import \{ redirect \} from "next/navigation";
export async function createPost(formData: FormData) \{
const title = formData.get("title") as string;
const body = formData.get("body") as string;
// Siempre valida en el servidor
if (!title || title.length < 3) \{
return \{ error: "El título debe tener al menos 3 caracteres" \};
\}
await db.post.create(\{ data: \{ title, body \} \});
revalidatePath("/posts");
redirect("/posts");
\}Reglas para evitar waterfalls
- Nunca hagas await secuencial cuando los fetches son independientes - Usa Promise.all
- Empuja la obtención de datos a los componentes hoja - Deja que cada componente obtenga sus propios datos
- Envuelve los componentes lentos en Suspense - Transmítelos de forma independiente
- Patrones de preload - Llama a las funciones fetch temprano, haz await después
- Usa parallel routes - Cada @slot carga de forma independiente con su propio loading.tsx
Patrón de preload
// lib/data.ts
export function preloadUser(id: string) \{
void getUser(id); // dispara el fetch, descarta la promesa (la memoización de solicitudes lo almacena en caché)
\}
// app/user/[id]/page.tsx
import \{ preloadUser \} from "@/lib/data";
export default async function UserPage(\{
params,
\}: \{
params: Promise<\{ id: string \}>;
\}) \{
const \{ id \} = await params;
preloadUser(id); // empieza a obtener datos inmediatamente
// ... otro trabajo ...
const user = await getUser(id); // usa la caché de memoización
return <UserProfile user=\{user\} />;
\}
## Ejemplo funcional
### Ejemplo 1: El usuario pregunta "¿Cómo debería obtener datos para mi panel?"
**Prompt del usuario:** "Mi panel tiene información del usuario, actividad reciente y gráficos de analíticas. Es lento porque todo carga de forma secuencial."
**La respuesta guiada por el skill incluiría:**
```tsx
// app/dashboard/page.tsx
import { Suspense } from "react";
import { UserInfoSkeleton, ActivitySkeleton, ChartSkeleton } from "./skeletons";
export default function DashboardPage() {
return (
<div className="grid grid-cols-12 gap-6">
<div className="col-span-4">
<Suspense fallback={<UserInfoSkeleton />}>
<UserInfo />
</Suspense>
</div>
<div className="col-span-8">
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart />
</Suspense>
</div>
<div className="col-span-12">
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
</div>
</div>
);
}
// Cada componente obtiene datos de forma independiente - sin waterfalls
async function UserInfo() {
const user = await getCurrentUser();
return <UserCard user={user} />;
}
async function AnalyticsChart() {
const data = await getAnalytics();
return <Chart data={data} />;
}
async function RecentActivity() {
const activity = await getActivity();
return <ActivityFeed items={activity} />;
}
Ejemplo 2: El usuario pregunta "¿Cómo gestiono la caché de mi catálogo de productos?"
La respuesta guiada por el skill recomendaría:
- Usa ISR con
revalidate: 3600para listados de productos - Usa revalidación bajo demanda mediante tags cuando se actualicen productos
- Usa
generateStaticParamspara los productos más populares - Transmite contenido dinámico (precio, stock) con Suspense
Profundización
Cómo funciona el skill
Este skill equipa a Claude con:
- Matriz de decisión - Asigna cada escenario de obtención de datos a la estrategia correcta
- Conocimiento de capas de caché - Las cuatro capas de caché de Next.js y cómo interactúan
- Prevención de waterfalls - Patrones y reglas para eliminar la obtención secuencial
- Patrones de streaming - Cómo usar límites Suspense para renderizado progresivo
Personalización
Extiende este skill añadiendo:
- Tus patrones específicos de ORM (Prisma, Drizzle, etc.)
- Wrappers de caché personalizados usados en tu proyecto
- Configuraciones específicas de clientes API
- Convenciones de obtención de datos (p. ej., "siempre usa nuestro wrapper
fetchApi")
Cómo instalar
mkdir -p .claude/skills/nextjs-data-fetching
# Pega el contenido de la Receta en .claude/skills/nextjs-data-fetching/SKILL.mdErrores comunes
- Next.js 15 cambió los valores por defecto de fetch - fetch ya no se almacena en caché por defecto. Es un cambio incompatible respecto a Next.js 14, donde
force-cacheera el valor por defecto. - La memoización de solicitudes solo funciona dentro de un único renderizado - No persiste entre solicitudes de distintos usuarios.
- revalidatePath revalida todos los segmentos dinámicos -
revalidatePath("/posts/[id]")revalida TODOS los posts, no solo uno. - Las Server Actions deben devolver datos serializables - No puedes devolver instancias de clase, funciones ni Dates.
- searchParams hace que una ruta sea dinámica - Usar searchParams excluye toda la ruta del renderizado estático.
Alternativas
| Enfoque | Cuándo usarlo |
|---|---|
| SWR | Fetch del lado del cliente con revalidación automática |
| TanStack Query | Gestión compleja de caché del lado del cliente |
| tRPC | Capa API type-safe de extremo a extremo |
| GraphQL (Apollo o Relay) | Requisitos de datos complejos con relaciones |
FAQs
¿Qué cambió en los valores por defecto de caché de fetch en Next.js 15?
- En Next.js 14,
fetchusabaforce-cachepor defecto (las solicitudes se almacenaban en caché) - En Next.js 15,
fetchusano-storepor defecto (las solicitudes NO se almacenan en caché) - Debes hacer opt-in explícitamente con
cache: "force-cache"onext: { revalidate: N }
¿Cómo evitas waterfalls de obtención de datos secuenciales en Server Components?
// Usa Promise.all para fetches independientes
const [user, posts] = await Promise.all([
getUser(),
getPosts(),
]);- Nunca hagas
awaitsecuencial cuando los fetches son independientes - Alternativamente, empuja la obtención a componentes hoja y envuelve cada uno en
<Suspense>
¿Qué es la memoización de solicitudes y cuánto dura?
- La misma URL de fetch + opciones en el mismo árbol de renderizado se deduplican automáticamente
- Solo dura durante una única solicitud del servidor
- No persiste entre solicitudes de distintos usuarios
- No se necesita configuración
¿Cómo tipas la prop searchParams en un componente de página de Next.js 15?
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; page?: string }>;
}) {
const { q, page } = await searchParams;
// ...
}searchParamses unaPromiseen Next.js 15+ y debe hacerse await
¿Cuál es la diferencia entre revalidación basada en tiempo y bajo demanda?
- Basada en tiempo:
fetch(url, { next: { revalidate: 60 } })regenera después de 60 segundos - Bajo demanda por ruta:
revalidatePath("/posts")invalida inmediatamente cuando se llama - Bajo demanda por tag: etiqueta los fetches con
next: { tags: ["posts"] }, luego llamarevalidateTag("posts")
Trampa: ¿revalidatePath("/posts/[id]") revalida solo un post?
- No.
revalidatePath("/posts/[id]")revalida TODAS las páginas que coinciden con ese segmento dinámico - No apunta a un solo post por ID
- Usa revalidación basada en tags si necesitas invalidar un recurso específico
¿Cómo combina Partial Prerendering (PPR) contenido estático y dinámico?
- Las partes estáticas de la página se prerrenderizan en tiempo de compilación y se sirven desde la CDN
- Las partes dinámicas se envuelven en límites
<Suspense>con fallbacks skeleton - El contenido dinámico se transmite en tiempo de solicitud sin bloquear el shell estático
- Requiere
experimental: { ppr: true }en next.config.ts
¿Qué es el patrón preload y por qué es útil?
export function preloadUser(id: string) {
void getUser(id); // dispara el fetch, descarta la promesa
}
// Más adelante en el componente:
preloadUser(id);
const user = await getUser(id); // usa la caché de memoización- Inicia el fetch temprano para que los datos estén listos cuando realmente hagas await
- Funciona porque la memoización de solicitudes deduplica el mismo fetch en un renderizado
Trampa: ¿pueden las Server Actions devolver objetos Date o instancias de clase?
- No. Las Server Actions deben devolver solo datos serializables
- No puedes devolver instancias de clase, funciones ni objetos Date
- Convierte las Dates a cadenas ISO y las instancias de clase a objetos planos antes de devolver
¿Cómo almacenas en caché datos que no son fetch (p. ej., consultas a base de datos) en Next.js?
import { unstable_cache } from "next/cache";
const getCachedUser = unstable_cache(
async (id: string) => db.user.findUnique({ where: { id } }),
["user"],
{ revalidate: 3600, tags: ["user"] }
);- Usa
unstable_cachepara consultas ORM y otras fuentes de datos que no son fetch - Proporciona partes de la clave de caché y configuración opcional de revalidación/tags
¿Qué hace usar searchParams en la estrategia de renderizado de una ruta?
- Usar
searchParamshace que la ruta sea dinámica - Toda la ruta queda excluida del renderizado estático y se renderiza por solicitud
- Esto aplica incluso si el resto de la página podría ser estática
¿Cómo deberías tipar una Server Action que recibe FormData?
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
// Siempre valida en el servidor con Zod
const parsed = Schema.safeParse({ title });
if (!parsed.success) return { error: "No válido" };
}- El parámetro se tipa como
FormData, no como un objeto tipado - Siempre parsea y valida con Zod en el lado del servidor