How to Fix: `revalidatePath()` does not revalidate layout when followed by `redirect()`
revalidatePath() can appear to “fail” when it is immediately followed by redirect(), but the real problem is more specific: the redirected navigation does not necessarily cause the invalidated layout segment to be fetched again in the way you expect. The result is stale layout data, even though your server action did trigger revalidation.
Understanding the Root Cause
In the Next.js App Router, revalidatePath() marks cached data for a route segment as stale. It does not force the current render tree to synchronously rebuild before the next line runs. When you call redirect() right after it inside a server action, Next.js throws a redirect response and short-circuits the current request lifecycle.
That behavior matters because layouts are cached and reused differently from pages. If your updated value is read in a parent layout, and the redirect lands on a route that can reuse the existing layout shell, the client may navigate without requesting a fresh version of that layout segment immediately. So although the path was invalidated, the redirected transition can still show stale layout state until a later navigation or refresh forces a new fetch.
In practical terms, the bug shows up like this:
- A server action updates data.
- revalidatePath() is called for a route whose layout reads that data.
- redirect() executes immediately after.
- The destination renders, but the parent layout remains stale.
This is why the issue is most visible when shared UI, such as a header, nav, counter badge, or account summary, lives in a layout instead of the page itself.
Step-by-Step Solution
The most reliable fix is to avoid depending on redirect() to refresh the invalidated layout in the same action flow. Instead, use one of these patterns:
- Revalidate a more precise target if possible.
- Move the data read from layout into the destination page if the refreshed value is only needed there.
- Trigger navigation on the client after the action completes, then call router.refresh().
- Use revalidateTag() with tagged fetches when your data model is shared across multiple segments.
Option 1: Client navigation after the server action
This is usually the safest workaround when the layout must visibly update right away.
'use server'
import { revalidatePath } from 'next/cache'
export async function updateCounter() {
await saveCounterValue()
revalidatePath('/dashboard')
return { success: true }
}
Then handle navigation in a client component:
'use client'
import { useRouter } from 'next/navigation'
import { updateCounter } from './actions'
export function SubmitButton() {
const router = useRouter()
async function onClick() {
const result = await updateCounter()
if (result?.success) {
router.push('/dashboard')
router.refresh()
}
}
return <button onClick={onClick}>Save and continue</button>
}
Why this works: router.refresh() tells the client to fetch a fresh React Server Component payload for the current route tree, which is much more likely to pull the newly invalidated layout data.
Option 2: Revalidate the exact layout path you depend on
If your stale state is in a parent segment, make sure you are revalidating the correct route path rather than only a child page.
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function updateSettings() {
await persistSettings()
revalidatePath('/dashboard')
redirect('/dashboard/account')
}
If the layout is attached to /dashboard, invalidating only /dashboard/account may not be enough. Revalidate the parent segment that owns the layout data.
Option 3: Use tagged data invalidation
If your layout and page both fetch the same underlying data, revalidateTag() can be a cleaner strategy than path-based invalidation.
import { revalidateTag } from 'next/cache'
export async function updateProfile() {
await saveProfile()
revalidateTag('profile')
}
And tag the fetch:
await fetch('https://example.com/api/profile', {
next: { tags: ['profile'] }
})
This approach reduces confusion when multiple route segments consume the same cached resource.
Option 4: Move volatile data out of the layout
If the changing value is highly interactive, a shared layout may be the wrong place to load it. Layouts are excellent for stable structural UI, but fast-changing session-like counters, temporary banners, or wizard state often behave better in a page-level server component or a client state layer.
export default async function DashboardPage() {
const counter = await getCounter()
return (
<section>
<h1>Dashboard</h1>
<p>Counter: {counter}</p>
</section>
)
}
If the stale value no longer lives in the shared layout, the redirect problem becomes much less noticeable.
Recommended fix for the reported issue
For the reproduction in this issue, the most practical solution is:
// server action
'use server'
import { revalidatePath } from 'next/cache'
export async function increment() {
await updateValueInStorage()
revalidatePath('/')
return { redirectTo: '/somewhere' }
}
// client component
'use client'
import { useRouter } from 'next/navigation'
import { increment } from './actions'
export function IncrementForm() {
const router = useRouter()
async function submit() {
const result = await increment()
router.push(result.redirectTo)
router.refresh()
}
return <button onClick={submit}>Increment</button>
}
This avoids the fragile combination of revalidatePath() followed immediately by redirect() in the same server action.
Common Edge Cases
- Revalidating the wrong segment: If the stale value is rendered by a parent layout, invalidating only the child route will not reliably refresh that parent layout.
- Mixing static and dynamic rendering: If part of the tree is statically cached and another part is dynamic, the visible behavior can seem inconsistent across refreshes and redirects.
- Using cookies() or headers() in layouts: These APIs can make segments dynamic, which changes cache behavior and may hide or expose the issue differently.
- Client-side caches still holding old data: If you also use SWR, React Query, or custom client state, a successful server revalidation may still be masked by stale client cache.
- Tagged fetches not matching: If you call revalidateTag() but your fetch requests are missing the correct tag, nothing will update.
- Assuming redirect waits for fresh render: It does not. redirect() changes control flow; it is not a guarantee that the destination will rebuild every cached ancestor immediately.
FAQ
Does this mean revalidatePath() is broken?
No. In this scenario, revalidatePath() is usually invalidating data correctly, but the subsequent redirect() and App Router cache reuse can prevent the affected layout from being refetched right away.
Should I always replace redirect() with router.push()?
Not always. redirect() is still appropriate for many server-side flows. Use client-side navigation plus router.refresh() when you specifically need the redirected route tree, especially a shared layout, to reflect freshly invalidated data immediately.
Is revalidateTag() better than revalidatePath() for this bug?
It can be. If the stale content comes from shared fetched resources rather than route-specific rendering, revalidateTag() is often more predictable and easier to reason about than path invalidation across nested layouts.
If you want the shortest path to a fix, treat this issue as a cache invalidation plus navigation timing problem: invalidate the correct segment or tag, then perform navigation in a way that forces a fresh route tree fetch instead of relying on an immediate server-side redirect() to update cached layouts.