Revalidación
Actualiza datos almacenados en caché bajo demanda o en un temporizador con revalidatePath, revalidateTag e ISR.
Receta
Tarjeta de referencia rápida -- lista para copiar y pegar.
// Revalidación basada en tiempo (ISR)
await fetch(url, { next: { revalidate: 60 } }); // revalidar cada 60 segundos
// Obtención basada en etiquetas
await fetch(url, { next: { tags: ["products"] } });
// Revalidación bajo demanda por etiqueta
import { revalidateTag } from "next/cache";
revalidateTag("products");
// Revalidación bajo demanda por ruta
import { revalidatePath } from "next/cache";
revalidatePath("/products"); // revalidar una página específica
revalidatePath("/products", "layout"); // revalidar layout y todas las páginas hijas
revalidatePath("/", "layout"); // revalidar toda la aplicaciónCuándo usarlo: Tienes datos almacenados en caché que necesitan actualizarse -- ya sea periódicamente (ISR) o inmediatamente después de una mutación.
Ejemplo Funcional
// lib/products.ts
export async function getProducts() {
const res = await fetch("https://api.example.com/products", {
next: { revalidate: 300, tags: ["products"] },
});
if (!res.ok) throw new Error("Failed to fetch products");
return res.json() as Promise<Product[]>;
}
export async function getProduct(slug: string) {
const res = await fetch(`https://api.example.com/products/${slug}`, {
next: { tags: ["products", `product-${slug}`] },
});
if (!res.ok) throw new Error("Product not found");
return res.json() as Promise<Product>;
}// app/actions/product.ts
"use server";
import { revalidateTag, revalidatePath } from "next/cache";
import { db } from "@/lib/db";
export async function updateProduct(formData: FormData) {
const slug = formData.get("slug") as string;
const name = formData.get("name") as string;
const price = Number(formData.get("price"));
await db.product.update({
where: { slug },
data: { name, price },
});
// Revalidar solo los datos en caché de este producto
revalidateTag(`product-${slug}`);
// También revalidar la página de listado de productos
revalidatePath("/products");
}
export async function deleteProduct(slug: string) {
await db.product.delete({ where: { slug } });
// Revalidar todos los cachés relacionados con productos
revalidateTag("products");
}// app/api/revalidate/route.ts -- revalidación disparada por webhook
import { revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const secret = request.headers.get("x-revalidation-secret");
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { tag } = await request.json();
revalidateTag(tag);
return NextResponse.json({ revalidated: true, tag });
}Lo que esto demuestra:
- ISR basado en tiempo con
next.revalidatepara actualizar en segundo plano - Revalidación granular basada en etiquetas después de una mutación
- Revalidación basada en ruta para actualizar una página completa
- Revalidación disparada por webhook desde un CMS externo
Análisis Profundo
Cómo Funciona
- Revalidación basada en tiempo (ISR): Cuando se establece
next: { revalidate: N }, Next.js sirve la versión en caché duranteNsegundos. Después de que vence el tiempo, la siguiente solicitud sigue obteniendo la versión obsoleta (respuesta instantánea), pero dispara una regeneración en segundo plano. Las solicitudes posteriores obtienen la versión fresca. Este es el patrón "stale-while-revalidate". - Revalidación bajo demanda con
revalidateTag: Cada llamada afetchpuede etiquetarse connext: { tags: ["my-tag"] }. Llamar arevalidateTag("my-tag")purga todas las entradas en caché con esa etiqueta. La siguiente solicitud dispara una obtención fresca. - Revalidación bajo demanda con
revalidatePath: Purga la caché de ruta completa para una ruta específica. Cuando se llama con un segundo argumento de"layout", también revalida todas las páginas anidadas bajo ese layout. - Tanto
revalidateTagcomorevalidatePathpueden llamarse dentro de Server Actions, Route Handlers o Middleware. - La revalidación no ocurre durante la solicitud actual -- marca la caché como obsoleta para que la siguiente solicitud dispare la regeneración.
Variaciones
Configuración de revalidación a nivel de segmento:
// app/products/layout.tsx
// Todas las páginas bajo /products se revalidan cada 5 minutos
export const revalidate = 300;Revalidar datos que no son fetch con unstable_cache:
import { unstable_cache } from "next/cache";
import { db } from "@/lib/db";
const getCachedUser = unstable_cache(
async (id: string) => db.user.findUnique({ where: { id } }),
["user-cache"],
{ tags: ["users"], revalidate: 600 }
);Forzar renderización dinámica (optar por no almacenar en caché):
// app/dashboard/page.tsx
export const dynamic = "force-dynamic";Notas de TypeScript
// firma de revalidatePath
function revalidatePath(
originalPath: string,
type?: "layout" | "page"
): void;
// firma de revalidateTag
function revalidateTag(tag: string): void;
// tipos de exportación de configuración de segmento
export const revalidate: number | false = 60;
export const dynamic: "auto" | "force-dynamic" | "force-static" | "error" = "auto";Problemas Comunes
-
revalidatePathno se revalida instantáneamente -- Marca la caché como obsoleta. La respuesta actual sigue sirviendo datos obsoletos. Los datos frescos aparecen en la siguiente solicitud. Solución: Entiende el modelo de stale-while-revalidate; si necesitas datos frescos inmediatos en la respuesta actual, usacache: "no-store". -
Las etiquetas son globales -- Llamar a
revalidateTag("data")purga cada entrada en caché etiquetada con"data"en toda la aplicación. Solución: Usa etiquetas específicas con espacios de nombres como"product-abc123"en lugar de genéricas. -
revalidatePath("/")no revalida todo -- Solo revalida la página raíz. Solución: UsarevalidatePath("/", "layout")para revalidar el layout raíz y todas las páginas hijas. -
ISR
revalidate: 0significa sin caché -- Establecer revalidate en0opta por renderización dinámica en la ruta, no por "revalidar inmediatamente". Solución: Usa un entero positivo para ISR; usacache: "no-store"para datos verdaderamente dinámicos. -
Múltiples tiempos de revalidación en la misma ruta -- Si diferentes obtenciones en la misma ruta usan diferentes valores de
revalidate, Next.js usa el valor más bajo para toda la ruta. Solución: Sé consistente con intervalos de revalidación dentro de una ruta, o divide en límites Suspense separados.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
cache: "no-store" | Los datos deben estar frescos en cada solicitud | Puedes tolerar datos ligeramente obsoletos |
| Sondeo del lado cliente (SWR/React Query) | Necesitas actualizaciones en tiempo real en el navegador | La revalidación del lado servidor es suficiente |
Webhook + revalidateTag | Un sistema externo (CMS, pago) dispara purga de caché | Controlas la mutación en tus propias Server Actions |
revalidatePath | Quieres actualizar una página completa independientemente de etiquetas | Necesitas control granular sobre datos específicos |
force-dynamic completo | Cada dato en la ruta debe ser dinámico | Algunas partes de la página pueden ser estáticas |
Preguntas Frecuentes
¿Cuál es la diferencia entre revalidatePath y revalidateTag?
revalidatePathpurga la caché de ruta completa para una ruta de URL específicarevalidateTagpurga todas las entradas en caché etiquetadas con una cadena específica- Las etiquetas ofrecen invalidación granular y dirigida; las rutas actualizan la página completa
¿Hacen que revalidatePath o revalidateTag la respuesta actual devuelva datos frescos?
- No. Ambas marcan la caché como obsoleta para la siguiente solicitud
- La respuesta actual sigue sirviendo datos obsoletos
- Si necesitas datos frescos inmediatos en la respuesta actual, usa
cache: "no-store"
¿Qué revalida realmente revalidatePath("/")?
- Solo revalida la página raíz (
/), no la aplicación completa - Para revalidar el layout raíz y todas las páginas hijas, usa
revalidatePath("/", "layout")
¿Qué sucede cuando diferentes obtenciones en la misma ruta usan diferentes valores de revalidate?
- Next.js usa el valor más bajo para toda la ruta
- Por ejemplo, si una obtención usa
revalidate: 60y otra usarevalidate: 300, la ruta se revalida cada 60 segundos - Sé consistente o divide obtenciones en límites Suspense separados
¿Cómo configuras la revalidación disparada por webhook desde un CMS externo?
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const secret = request.headers.get("x-revalidation-secret");
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { tag } = await request.json();
revalidateTag(tag);
return NextResponse.json({ revalidated: true });
}¿Qué hace establecer revalidate: 0?
- Opta por renderización dinámica en la ruta, no por "revalidar inmediatamente"
- Efectivamente lo mismo que
cache: "no-store" - Usa un entero positivo para comportamiento ISR
¿Cómo escribes la firma de la función revalidatePath en TypeScript?
function revalidatePath(
originalPath: string,
type?: "layout" | "page"
): void;- El segundo argumento es opcional y por defecto es
"page" - Pasa
"layout"para revalidar el layout y todas las páginas anidadas
¿Puedes revalidar fuentes de datos que no son fetch como consultas de base de datos?
- Envuelve la consulta con
unstable_cachedenext/cachey asigna etiquetas - Luego llama a
revalidateTagcon la misma etiqueta para invalidar
import { unstable_cache } from "next/cache";
const getCachedUser = unstable_cache(
async (id: string) => db.user.findUnique({ where: { id } }),
["user-cache"],
{ tags: ["users"], revalidate: 600 }
);¿Qué es la exportación de configuración revalidate a nivel de segmento y qué hace?
// app/products/layout.tsx
export const revalidate = 300;- Establece el intervalo de revalidación predeterminado para todas las páginas bajo ese layout
- Las llamadas a fetch individuales aún pueden anular este valor
¿Las etiquetas están limitadas por ruta o son globales en toda la aplicación?
- Las etiquetas son globales -- llamar a
revalidateTag("data")purga cada entrada en caché con esa etiqueta en toda la aplicación - Usa etiquetas específicas con espacios de nombres como
"product-abc123"para evitar purgas de caché no intencionadas
¿De dónde pueden llamarse revalidateTag y revalidatePath?
- Server Actions
- Route Handlers
- Middleware
- No pueden llamarse desde componentes Cliente o durante renderización del lado cliente
¿Cómo funciona el ISR basado en tiempo (stale-while-revalidate) internamente?
- Next.js sirve la versión en caché durante N segundos después de la última generación
- Después de que vence el tiempo, la siguiente solicitud sigue obteniendo la versión obsoleta instantáneamente
- Se dispara una regeneración en segundo plano, y las solicitudes posteriores reciben la versión fresca
Relacionado
- Fetching -- Configurar fetch con opciones de caché
- Server Actions -- Llamar a revalidación después de mutaciones
- Caching -- Entender la capa de caché de datos
- Static vs Dynamic Rendering -- Cómo la revalidación afecta el modo de renderización