revalidatePath & revalidateTag
revalidatePath and revalidateTag, both imported from next/cache, are the two building blocks of on-demand revalidation. They let you tell Next.js precisely which cached data (and cached rendered routes) are now stale, right at the moment you know a mutation happened — instead of waiting for a time-based window to elapse.
revalidatePath: invalidate by path
revalidatePath('/some/path') clears the cached data and rendered HTML for that specific route (and, with the 'layout' type, everything nested under it), so the next request to that path triggers a fresh render.
'use server'
import { revalidatePath } from 'next/cache'
export async function updatePostTitle(slug: string, title: string) {
await db.post.update({
where: { slug },
data: { title },
})
// Only this specific blog post's page is invalidated
revalidatePath(`/blog/${slug}`)
}revalidatePath accepts an optional second argument to control how broadly it invalidates: 'page' (the default) invalidates only the exact route, while 'layout' also invalidates every route nested under the matching layout.
// Invalidates every route under this layout, e.g. every page in /blog/*
revalidatePath('/blog', 'layout')revalidateTag: invalidate by tag, across routes
Sometimes the same piece of data is rendered on several different routes — a product might appear on its own detail page, a category listing, and the homepage. Tracking down and calling revalidatePath for every one of those routes is fragile. revalidateTag solves this by invalidating any cached fetch anywhere in the app that was tagged with a matching string, regardless of which route it lives on.
// Wherever this product is fetched, tag it
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { tags: [`product-${id}`, 'products'] },
})
return res.json()
}'use server'
import { revalidateTag } from 'next/cache'
export async function updateProductPrice(id: string, price: number) {
await db.product.update({ where: { id }, data: { price } })
// Invalidates this product everywhere it's fetched, on every route,
// without needing to know which paths reference it.
revalidateTag(`product-${id}`)
}revalidatePath vs. revalidateTag
Function | Invalidates | Best when |
|---|---|---|
revalidatePath | A specific route path (optionally its whole layout subtree) | The data change maps cleanly onto one known URL |
revalidateTag | Every fetch anywhere tagged with that string, across any number of routes | The same data appears on multiple, possibly unknown, routes |
Worked example: updating a blog post
A typical blog editing flow needs to invalidate both the individual post page and the listing page that shows post previews. Combining revalidatePath calls (or a shared tag) covers both.
// app/[locale]/blog/[slug]/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function updatePost(slug: string, formData: FormData) {
const title = formData.get('title') as string
const body = formData.get('body') as string
await db.post.update({
where: { slug },
data: { title, body },
})
// The post's own page...
revalidatePath(`/blog/${slug}`)
// ...and the listing page that shows its title/preview
revalidatePath('/blog')
redirect(`/blog/${slug}`)
}