Skip to main content

Revalidation and Redirects in Server Actions

After a Server Action mutates data (create, update, delete), you need to refresh cached pages so users see the new data. Next.js provides revalidatePath() and revalidateTag() to invalidate caches on demand. revalidatePath() invalidates all cached pages matching a path pattern, while revalidateTag() invalidates pages tagged with a specific string. After revalidation, Next.js re-renders those pages on the next request, fetching fresh data from the database.

The redirect() function navigates the user to a new URL after a mutation completes. Without JavaScript, it's an HTTP redirect. With JavaScript, you can handle it client-side with useRouter.push(). Together, revalidation and redirects create a seamless workflow: mutate data, invalidate caches, and navigate to a confirmation or list page.

Understanding Next.js Caching

Next.js caches several things: static pages (pre-rendered at build time), ISR pages (cached with a revalidation interval), and data fetches using fetch() with cache options. When you mutate data, the cache becomes stale and returns old data until you revalidate.

Static Generation (default): Pages are rendered once at build time and cached forever. To update them, you revalidate.

Incremental Static Regeneration (ISR): Pages are cached for a duration (e.g., 60 seconds), then re-rendered on the next request. Useful for frequently-updated content.

Dynamic rendering: Pages are rendered on every request (no caching). Use this when data changes frequently or personalization is needed.

Using revalidatePath()

revalidatePath() invalidates the cache for a specific path. Call it in a Server Action after mutating data:

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

export async function addTask(formData: FormData) {
const title = formData.get('title') as string;

const task = await db.tasks.create({ title });

// Invalidate the tasks list page so it refetches on next request
revalidatePath('/tasks');

return { success: true, taskId: task.id };
}

revalidatePath('/tasks') invalidates the cache for /tasks (and its layout and parent pages). On the next request to /tasks, Next.js re-renders the page, fetching fresh data from the database.

Wildcard paths: Use a glob pattern to invalidate multiple paths:

// Invalidate all paths starting with /blog
revalidatePath('/blog/[slug]', 'page');

// Invalidate all dynamic routes under /admin
revalidatePath('/admin/.*', 'page');

The second argument is the type: 'page', 'layout', or 'all' (default). Use 'page' to invalidate only the page component, or 'layout' for the layout and child pages.

Using revalidateTag()

revalidateTag() is more flexible. You tag data fetches in your code, then invalidate all pages using that tag:

// app/lib/db.ts
export async function getTasks() {
return fetch('https://api.example.com/tasks', {
next: { tags: ['tasks'] }, // Tag this fetch
});
}

export async function getTask(id: string) {
return fetch(`https://api.example.com/tasks/${id}`, {
next: { tags: ['tasks', `task-${id}`] }, // Multiple tags
});
}

Then in your Server Action, invalidate the tag:

// app/actions.ts
'use server';

import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';

export async function addTask(formData: FormData) {
const title = formData.get('title') as string;
await db.tasks.create({ title });

// Invalidate all pages tagged with 'tasks'
revalidateTag('tasks');

return { success: true };
}

revalidateTag('tasks') invalidates the cache for all fetches tagged with 'tasks'. This is more powerful than revalidatePath() because it's independent of the URL structure. If tasks appear on /tasks, /dashboard, and /api/tasks, tagging handles all of them.

Redirecting After Mutations

After a mutation, use redirect() to navigate the user:

// app/actions.ts
'use server';

import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;

const post = await db.posts.create({ title, content });

revalidatePath('/blog');

// Redirect to the new post
redirect(`/blog/${post.slug}`);
}

redirect() throws an error that Next.js catches and converts to an HTTP redirect (without JavaScript) or client-side navigation (with JavaScript). The user is taken to the new URL.

Important: redirect() must be called in a Server Action or Server Component. It doesn't work in Client Components. If you need to redirect from a Client Component after a Server Action, use the useRouter hook:

// app/components/CreatePostForm.tsx
'use client';

import { useRouter } from 'next/navigation';
import { useActionState } from 'react';
import { createPost } from '@/app/actions';

export function CreatePostForm() {
const router = useRouter();
const [state, formAction] = useActionState(
async (formData: FormData) => {
try {
const result = await createPost(formData);
// createPost calls redirect(), which throws
// But if it returned a value, we'd navigate here:
if (result?.postId) {
router.push(`/blog/${result.postId}`);
}
} catch (error) {
return { error: String(error) };
}
},
{ error: null }
);

return (
<form action={formAction}>
{/* form fields */}
</form>
);
}

Comparing Revalidation Strategies

StrategyUse CasePerformanceComplexity
revalidatePath('/path')Small, focused mutations (add a comment)Refreshes once per pathLow
revalidateTag('posts')Related data across multiple pagesRefreshes all tagged fetchesMedium
On-demand ISR (manual)Admin actions, bulk updatesMaximum controlHigh
Dynamic renderingReal-time data, personalizationAlways fresh, no cachingHigh

For most applications, revalidateTag() is more maintainable than revalidatePath() because it's agnostic to URL structure. If you move pages around or add new pages displaying the same data, tags still work.

Error Handling with Redirects

If a redirect might fail (e.g., invalid data), return a value instead and let the client decide:

// app/actions.ts
'use server';

export async function createTask(formData: FormData) {
const title = formData.get('title') as string;

if (!title.trim()) {
return { error: 'Title is required', taskId: null };
}

const task = await db.tasks.create({ title });

// Return taskId instead of redirect
return { error: null, taskId: task.id };
}
// app/components/AddTaskForm.tsx
'use client';

import { useRouter } from 'next/navigation';

export function AddTaskForm() {
const router = useRouter();

async function handleSubmit(formData: FormData) {
const result = await createTask(formData);

if (result.error) {
// Show error, don't navigate
console.error(result.error);
} else if (result.taskId) {
// Navigate only if successful
router.push(`/tasks/${result.taskId}`);
}
}

return <form action={handleSubmit}>{/* ... */}</form>;
}

Key Takeaways

  • After mutating data in a Server Action, call revalidatePath() or revalidateTag() to invalidate caches and fetch fresh data on the next request.
  • Use revalidateTag() for data that appears across multiple pages; use revalidatePath() for focused, single-page mutations.
  • Tag data fetches with next: { tags: ['tag-name'] } to manage related data across your app.
  • Use redirect() in Server Actions to navigate users; use useRouter.push() in Client Components when you need conditional navigation.
  • Always validate and error-check before redirecting. Return values from actions to let the client decide whether to navigate.

Frequently Asked Questions

Can I revalidate on a schedule (not just after mutations)?

Yes, use ISR with a revalidate option: export const revalidate = 60 at the top of a page file to re-render every 60 seconds. Or use next: { revalidate: 60 } in a fetch() call. However, this is automatic—you don't need a Server Action for scheduled revalidation.

What's the difference between revalidatePath() and revalidateTag()?

revalidatePath('/path') invalidates a specific path and its layout. revalidateTag('tag') invalidates all data fetches tagged with that tag, regardless of path. Use tags for data-centric invalidation; use paths for URL-centric invalidation.

If I call redirect() and an error occurs, what happens?

redirect() throws an error that Next.js catches. If another error is thrown before redirect(), the action fails and the redirect doesn't happen. The error is returned to the client, where you can catch and display it.

Can I pass data through a redirect?

No—redirects are HTTP mechanisms that lose context. Instead, mutate the data (e.g., create a database record), revalidate the destination page, and redirect. The destination page fetches the new data and displays it.

How do I invalidate data on a static page that's been pre-rendered?

You must revalidate it. Call revalidatePath() with the exact path or a glob pattern. Next.js adds the page to a revalidation queue; on the next request, it re-renders. Without a request, the page remains cached until the next deployment or manual revalidation.

Further Reading