Next.js caches data for performance, but after mutations (create, update, delete), you need to update the cache so users see fresh data. Next.js provides revalidatePath and revalidateTag for on-demand cache invalidation—purging stale data and refetching fresh content instantly. Master revalidation and your apps will feel fast while always showing current data!
Why Revalidate?
❌ Without Revalidation
// Server Action creates post
export async function createPost(formData: FormData) {
await db.posts.create({
data: { title: formData.get('title') },
});
// No revalidation!
}
// Blog page with cached data
export default async function BlogPage() {
const posts = await db.posts.findMany();
// Uses cached version - doesn't show new post!
return <PostList posts={posts} />;
}
// Problems:
// ❌ Users see stale data
// ❌ New post not visible until cache expires
// ❌ Confusing UX (created but not shown)
// ❌ Manual refresh required✅ With Revalidation
import { revalidatePath } from 'next/cache';
// Server Action creates post
export async function createPost(formData: FormData) {
await db.posts.create({
data: { title: formData.get('title') },
});
// Revalidate blog page!
revalidatePath('/blog');
}
// Blog page with cached data
export default async function BlogPage() {
const posts = await db.posts.findMany();
// Cache invalidated - fetches fresh data!
return <PostList posts={posts} />;
}
// Benefits:
// ✅ Users see fresh data immediately
// ✅ New post visible right away
// ✅ No manual refresh needed
// ✅ Great UXHow Revalidation Works
- Server Action mutates data (create/update/delete)
- Call revalidatePath() or revalidateTag()
- Next.js purges the cached data for that path/tag
- Next request fetches fresh data from source
- New cache generated with updated data
revalidatePath - Path-Based Revalidation
Basic Usage
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
// Create post
await db.posts.create({
data: { title },
});
// Revalidate the blog list page
revalidatePath('/blog');
}
// ✅ Purges cache for /blog
// ✅ Next visit fetches fresh data
// ✅ New post appears immediatelyRevalidating Multiple Paths
'use server';
import { revalidatePath } from 'next/cache';
export async function updatePost(postId: string, formData: FormData) {
const title = formData.get('title') as string;
const slug = formData.get('slug') as string;
// Update post
await db.posts.update({
where: { id: postId },
data: { title, slug },
});
// Revalidate multiple paths
revalidatePath('/blog'); // Blog list
revalidatePath(`/blog/${slug}`); // Individual post
revalidatePath('/'); // Homepage (if it shows posts)
}
// ✅ Revalidate all affected pages
// ✅ Ensures consistency across site
// ✅ Multiple revalidatePath calls allowedRevalidation Type: 'page' vs 'layout'
import { revalidatePath } from 'next/cache';
// Default: 'page' - revalidates single page
revalidatePath('/blog', 'page');
// 'layout' - revalidates page and all nested routes
revalidatePath('/blog', 'layout');
// Examples:
// revalidatePath('/blog', 'page')
// → Only revalidates /blog
// → /blog/post-1 still cached
// revalidatePath('/blog', 'layout')
// → Revalidates /blog
// → AND /blog/post-1
// → AND /blog/post-2
// → AND all nested routes
// Use 'layout' when:
// ✅ Mutation affects parent and children
// ✅ Shared layout data changed
// ✅ Want to refresh entire section
// Use 'page' (default) when:
// ✅ Only specific page affected
// ✅ More granular control
// ✅ Better performance (less revalidation)Complete CRUD Example
'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 slug = formData.get('slug') as string;
const content = formData.get('content') as string;
const post = await db.posts.create({
data: { title, slug, content },
});
// Revalidate blog list
revalidatePath('/blog');
// Redirect to new post
redirect(`/blog/${post.slug}`);
}
export async function updatePost(postId: string, formData: FormData) {
const title = formData.get('title') as string;
const slug = formData.get('slug') as string;
const content = formData.get('content') as string;
await db.posts.update({
where: { id: postId },
data: { title, slug, content },
});
// Revalidate both list and detail
revalidatePath('/blog');
revalidatePath(`/blog/${slug}`);
return { success: true };
}
export async function deletePost(postId: string) {
const post = await db.posts.findUnique({
where: { id: postId },
});
await db.posts.delete({
where: { id: postId },
});
// Revalidate list and detail page
revalidatePath('/blog');
revalidatePath(`/blog/${post.slug}`);
// Redirect to list
redirect('/blog');
}
export async function togglePublished(postId: string) {
const post = await db.posts.findUnique({
where: { id: postId },
});
await db.posts.update({
where: { id: postId },
data: { published: !post.published },
});
// Revalidate all pages showing this post
revalidatePath('/blog');
revalidatePath(`/blog/${post.slug}`);
revalidatePath('/'); // Homepage might show published posts
return { success: true };
}
// ✅ Complete CRUD with revalidation
// ✅ All affected pages updated
// ✅ Consistent data across siterevalidateTag - Tag-Based Revalidation
What Are Cache Tags?
Cache tags let you group related cache entries and revalidate them together. Instead of revalidating specific paths, you revalidate by semantic meaning (e.g., "all posts" or "user-123 data").
Adding Tags to Fetch Requests
export default async function BlogPage() {
// Fetch with cache tag
const posts = await fetch('https://api.example.com/posts', {
next: {
tags: ['posts'], // Tag this cache entry
},
}).then(res => res.json());
return <PostList posts={posts} />;
}
// Or with database queries:
export async function getPosts() {
'use cache';
const posts = await db.posts.findMany();
return posts;
}
// Add tags in route segment config:
export const dynamic = 'force-cache';
export const revalidate = 3600;
export const tags = ['posts'];
// ✅ Tag cache entries
// ✅ Revalidate by tag, not path
// ✅ More flexible than path-basedUsing revalidateTag
'use server';
import { revalidateTag } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.posts.create({
data: { title },
});
// Revalidate all cache entries tagged 'posts'
revalidateTag('posts');
}
export async function updatePost(postId: string, formData: FormData) {
const title = formData.get('title') as string;
await db.posts.update({
where: { id: postId },
data: { title },
});
// Revalidate posts and specific post
revalidateTag('posts');
revalidateTag(`post-${postId}`);
}
// ✅ Revalidate by semantic meaning
// ✅ Multiple pages with same tag updated
// ✅ More flexible than pathsMultiple Tags Strategy
// Fetch with multiple tags
export async function getPost(slug: string) {
const post = await fetch(`https://api.example.com/posts/${slug}`, {
next: {
tags: ['posts', `post-${slug}`, 'blog-content'],
},
}).then(res => res.json());
return post;
}
export async function getPostsByCategory(category: string) {
const posts = await fetch(`https://api.example.com/posts?category=${category}`, {
next: {
tags: ['posts', `category-${category}`],
},
}).then(res => res.json());
return posts;
}
export async function getUserPosts(userId: string) {
const posts = await fetch(`https://api.example.com/users/${userId}/posts`, {
next: {
tags: ['posts', `user-${userId}-posts`],
},
}).then(res => res.json());
return posts;
}
// ✅ Multiple tags per request
// ✅ Granular control
// ✅ Flexible revalidation'use server';
import { revalidateTag } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const category = formData.get('category') as string;
const userId = formData.get('userId') as string;
await db.posts.create({
data: { title, category, userId },
});
// Revalidate multiple related tags
revalidateTag('posts'); // All posts
revalidateTag(`category-${category}`); // Posts in this category
revalidateTag(`user-${userId}-posts`); // User's posts
revalidateTag('blog-content'); // General blog content
}
export async function deletePost(postId: string) {
const post = await db.posts.findUnique({
where: { id: postId },
});
await db.posts.delete({
where: { id: postId },
});
// Revalidate all related tags
revalidateTag('posts');
revalidateTag(`post-${post.slug}`);
revalidateTag(`category-${post.category}`);
revalidateTag(`user-${post.userId}-posts`);
}
// ✅ Comprehensive revalidation
// ✅ All affected caches updated
// ✅ Tag-based granular controlRevalidation Patterns
Pattern 1: Revalidate Related Pages
// When updating a post, revalidate:
// 1. Blog list
// 2. Individual post page
// 3. Homepage (if showing recent posts)
// 4. Category page (if post is categorized)
export async function updatePost(postId: string, formData: FormData) {
const post = await db.posts.update({
where: { id: postId },
data: { /* ... */ },
});
revalidatePath('/blog');
revalidatePath(`/blog/${post.slug}`);
revalidatePath('/');
revalidatePath(`/category/${post.category}`);
}
// ✅ Comprehensive revalidation
// ✅ All affected pages updatedPattern 2: Conditional Revalidation
export async function togglePublished(postId: string) {
const post = await db.posts.findUnique({
where: { id: postId },
});
const newPublishedState = !post.published;
await db.posts.update({
where: { id: postId },
data: { published: newPublishedState },
});
// Always revalidate admin pages
revalidatePath('/admin/posts');
revalidatePath(`/admin/posts/${postId}`);
// Only revalidate public pages if now published
if (newPublishedState) {
revalidatePath('/blog');
revalidatePath(`/blog/${post.slug}`);
revalidatePath('/');
}
}
// ✅ Smart revalidation
// ✅ Only revalidate what's needed
// ✅ Better performancePattern 3: Batch Operations
export async function bulkDeletePosts(postIds: string[]) {
// Delete multiple posts
await db.posts.deleteMany({
where: {
id: { in: postIds },
},
});
// Single revalidation for all
revalidatePath('/blog', 'layout'); // Revalidates all blog pages
revalidatePath('/'); // Homepage
// Or use tags
revalidateTag('posts');
revalidateTag('blog-content');
}
// ✅ Efficient batch revalidation
// ✅ One revalidation for multiple changes
// ✅ 'layout' type for nested routesPattern 4: Cross-Entity Revalidation
// When updating a user, also revalidate their posts
export async function updateUser(userId: string, formData: FormData) {
const name = formData.get('name') as string;
await db.users.update({
where: { id: userId },
data: { name },
});
// Revalidate user profile
revalidatePath(`/users/${userId}`);
// Revalidate user's posts (author name changed)
revalidateTag(`user-${userId}-posts`);
// Revalidate comments by user
revalidateTag(`user-${userId}-comments`);
}
// ✅ Cross-entity updates
// ✅ Maintains consistency
// ✅ Tags for related contentComplete Revalidation Examples
Example 1: E-commerce Product Updates
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function updateProductStock(productId: string, newStock: number) {
const product = await db.products.update({
where: { id: productId },
data: { stock: newStock },
});
// Revalidate product page
revalidatePath(`/products/${product.slug}`);
// Revalidate category page
revalidatePath(`/category/${product.categoryId}`);
// Revalidate products list
revalidatePath('/products');
// If out of stock, revalidate homepage
if (newStock === 0) {
revalidatePath('/');
}
// Tag-based revalidation
revalidateTag('products');
revalidateTag(`product-${productId}`);
revalidateTag(`category-${product.categoryId}`);
}
export async function updateProductPrice(productId: string, newPrice: number) {
const product = await db.products.update({
where: { id: productId },
data: { price: newPrice },
});
// Comprehensive revalidation for price changes
revalidatePath(`/products/${product.slug}`);
revalidatePath(`/category/${product.categoryId}`);
revalidatePath('/products');
revalidatePath('/'); // Homepage might show prices
revalidatePath('/deals'); // Deals page might be affected
revalidateTag('products');
revalidateTag(`product-${productId}`);
}
export async function deleteProduct(productId: string) {
const product = await db.products.findUnique({
where: { id: productId },
});
await db.products.delete({
where: { id: productId },
});
// Revalidate all pages
revalidatePath('/products', 'layout'); // All product pages
revalidatePath(`/category/${product.categoryId}`, 'layout');
revalidatePath('/');
revalidateTag('products');
}
// ✅ E-commerce revalidation
// ✅ Stock, price, delete handled
// ✅ All affected pages updatedExample 2: Social Media Posts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function createPost(userId: string, formData: FormData) {
const content = formData.get('content') as string;
const post = await db.posts.create({
data: {
userId,
content,
},
});
// Revalidate feeds
revalidatePath('/feed'); // Global feed
revalidatePath(`/users/${userId}`); // User profile
revalidateTag('posts');
revalidateTag(`user-${userId}-posts`);
return { success: true, postId: post.id };
}
export async function likePost(postId: string, userId: string) {
await db.likes.create({
data: { postId, userId },
});
// Increment like count
await db.posts.update({
where: { id: postId },
data: {
likes: { increment: 1 },
},
});
// Revalidate post and feeds
revalidateTag(`post-${postId}`);
revalidateTag('posts');
}
export async function addComment(postId: string, userId: string, content: string) {
await db.comments.create({
data: {
postId,
userId,
content,
},
});
// Increment comment count
await db.posts.update({
where: { id: postId },
data: {
commentCount: { increment: 1 },
},
});
// Revalidate post page (to show new comment)
revalidatePath(`/posts/${postId}`);
revalidateTag(`post-${postId}`);
revalidateTag('posts');
}
export async function deletePost(postId: string, userId: string) {
const post = await db.posts.findUnique({
where: { id: postId },
});
// Verify ownership
if (post.userId !== userId) {
throw new Error('Unauthorized');
}
await db.posts.delete({
where: { id: postId },
});
// Revalidate everything
revalidatePath('/feed');
revalidatePath(`/users/${userId}`);
revalidatePath(`/posts/${postId}`);
revalidateTag('posts');
revalidateTag(`user-${userId}-posts`);
}
// ✅ Social media patterns
// ✅ Likes, comments, deletes
// ✅ Real-time feel with revalidationRevalidation Project Structure
Organization with revalidation strategies
Select a file or folder to see details
Revalidation Best Practices
1. Always Revalidate After Mutations
// ✅ GOOD: Revalidate after mutation
export async function createPost(formData: FormData) {
await db.posts.create({ data: { /* ... */ } });
revalidatePath('/blog');
revalidatePath('/');
}
// ❌ BAD: No revalidation
export async function createPost(formData: FormData) {
await db.posts.create({ data: { /* ... */ } });
// Users see stale data!
}2. Revalidate All Affected Pages
// ✅ GOOD: Comprehensive revalidation
export async function updatePost(postId: string, formData: FormData) {
const post = await db.posts.update({ /* ... */ });
revalidatePath('/blog'); // List
revalidatePath(`/blog/${post.slug}`); // Detail
revalidatePath('/'); // Homepage
revalidatePath(`/category/${post.category}`); // Category
}
// ❌ BAD: Partial revalidation
export async function updatePost(postId: string, formData: FormData) {
await db.posts.update({ /* ... */ });
revalidatePath('/blog'); // Only list, detail still cached!
}3. Use Tags for Related Content
// ✅ GOOD: Tag-based for related content
export async function createPost(formData: FormData) {
await db.posts.create({ /* ... */ });
revalidateTag('posts'); // All posts everywhere
revalidateTag('blog-content'); // All blog content
}
// Better than revalidating many paths
revalidatePath('/blog');
revalidatePath('/blog/page/2');
revalidatePath('/blog/page/3');
// ... 50 more pages4. Use 'layout' Type for Nested Routes
// ✅ GOOD: Use 'layout' for nested routes
export async function deleteCategory(categoryId: string) {
await db.categories.delete({ where: { id: categoryId } });
// Revalidates /blog and all nested routes
revalidatePath('/blog', 'layout');
}
// Instead of:
revalidatePath('/blog');
revalidatePath('/blog/post-1');
revalidatePath('/blog/post-2');
// ... hundreds of posts5. Combine Path and Tag Revalidation
// ✅ GOOD: Use both for comprehensive coverage
export async function updatePost(postId: string, formData: FormData) {
const post = await db.posts.update({ /* ... */ });
// Path-based for specific pages
revalidatePath('/blog');
revalidatePath(`/blog/${post.slug}`);
// Tag-based for related content
revalidateTag('posts');
revalidateTag(`category-${post.category}`);
}
// Best of both approachesKey Takeaways
- revalidatePath - purge cache for specific paths
- revalidateTag - purge cache by semantic tags
- Always revalidate - after create, update, delete
- 'page' vs 'layout' - single page or nested routes
- Multiple paths - call revalidatePath multiple times
- Tags for flexibility - group related cache entries
- Comprehensive coverage - revalidate all affected pages
- Fresh data - users always see current content
What's Next?
You've mastered cache revalidation! Next, we'll explore Optimistic Updates—updating the UI immediately before Server Actions complete, providing instant feedback, and handling errors gracefully. You'll build interfaces that feel instant while maintaining data integrity!
We'll cover useOptimistic hook, rollback strategies, error handling, and creating responsive UIs that feel native-app fast.
⚡ Revalidation is Cheap
Don't worry about over-revalidating. Next.js caching is efficient, and revalidation is inexpensive. It's better to revalidate too much than too little. Fresh data is more important than cache hits!