How to Fix: Redirect function in server action ignores “replace” parameter
The redirect call inside a Server Action currently ignores the replace option because that navigation is not handled like a client-side router transition. Even if you call redirect('/child', 'replace') or use the equivalent API shape in a server action, the framework resolves the redirect through the server action response flow, which behaves like a fresh navigation and adds a browser history entry instead of replacing the current one.
Table of Contents
Understanding the Root Cause
This bug happens because Server Actions and client-side navigation do not use the same redirect mechanism.
In a normal client transition, Next.js can tell the browser router whether it should push a new history entry or replace the current one. That is what methods like router.push() and router.replace() are designed for.
Inside a server action, however, redirect() does not directly control the browser history stack. Instead, it throws a framework-level redirect signal that gets serialized into the server action response. When the client receives that response, it performs navigation based on the action result flow, and in the affected implementation the replace parameter is ignored. The result is that the browser behaves as though a push-style navigation occurred, even when replace was requested.
That is why reproducing this issue usually looks like this:
- The user navigates to a child route.
- A form triggers a Server Action.
- The server action calls
redirect()with a replace intent. - The app navigates correctly, but the browser history still contains the previous page.
So the core issue is not that redirect() fails completely. The redirect works, but the history replacement behavior is lost in the server action pipeline.
Step-by-Step Solution
The safest workaround is to move the history-sensitive navigation decision to the client, where router.replace() is fully supported and predictable.
Use this pattern:
- Let the server action complete its server-side work.
- Return a success result or target path instead of calling
redirect()with replace semantics. - In a client component, inspect the action result.
- Call
router.replace()manually.
1. Problematic pattern
'use server'
import { redirect } from 'next/navigation'
export async function submitAction() {
// perform mutation
redirect('/parent')
}
This works for navigation, but in the affected scenario it does not reliably replace browser history.
2. Recommended server action workaround
'use server'
export async function submitAction() {
// perform mutation
return {
success: true,
redirectTo: '/parent'
}
}
3. Handle the redirect in a client component
'use client'
import { useRouter } from 'next/navigation'
import { useState, useTransition } from 'react'
import { submitAction } from './actions'
export default function Form() {
const router = useRouter()
const [isPending, startTransition] = useTransition()
const [error, setError] = useState('')
async function onSubmit(formData) {
setError('')
startTransition(async () => {
const result = await submitAction(formData)
if (result?.success && result?.redirectTo) {
router.replace(result.redirectTo)
return
}
setError('Something went wrong')
})
}
return (
<form action={onSubmit}>
<button type="submit" disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit'}
</button>
{error ? <p>{error}</p> : null}
</form>
)
}
4. If you must redirect on the server
If the navigation is security-sensitive or must happen entirely on the server, you can still use redirect(), but you should treat it as a redirect that may behave like a push in browser history for this bug window. In other words, do not depend on the replace semantics until the framework version you use explicitly fixes the issue.
'use server'
import { redirect } from 'next/navigation'
export async function submitAction() {
// perform mutation
redirect('/parent')
}
Use this only when replacing the history entry is not critical to the user flow.
5. Best practice decision guide
- Use server-side redirect when the redirect must be enforced by the server.
- Use client-side router.replace() when browser history behavior matters.
- Use returned action state when you need both mutation and precise navigation control.
Common Edge Cases
1. Double submissions
If the user taps the submit button multiple times on mobile, your action may run more than once before navigation completes. Always disable the button while the action is pending.
<button type="submit" disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit'}
</button>
2. Redirect after failed validation
If validation fails on the server and you return field errors, do not call router.replace(). Only replace when the action result is truly successful.
if (result?.errors) {
setErrors(result.errors)
return
}
if (result?.redirectTo) {
router.replace(result.redirectTo)
}
3. Mixing redirect() and returned state
Do not both return data and call redirect() in the same successful code path. The redirect throws internally and short-circuits execution, so any returned object after it is unreachable.
4. Back button still shows unexpected pages
If you previously navigated with a Link to the child page, that history entry already exists. Replacing only affects the current navigation step, not the entire history stack. This is important when validating whether the workaround behaves correctly.
5. Version-specific behavior
This issue is tied to framework implementation details. Before keeping a workaround permanently, test against the exact Next.js version in your project and check the official issue tracker or release notes on Next.js GitHub for a fix.
FAQ
Does redirect() fail entirely in a server action?
No. The redirect usually works for navigation itself. The bug is that the replace behavior is ignored, so browser history is updated as if a new entry was pushed.
Why does router.replace() work when redirect() does not?
router.replace() runs on the client and talks directly to the app router’s client-side navigation layer, where history replacement is explicitly supported. A server action redirect goes through a different response-driven mechanism.
Should I stop using server actions for redirects?
No. Use server action redirects when the server must control the outcome. But if your requirement specifically depends on history replacement, return a result from the action and perform router.replace() in a client component until the framework fixes the bug.
In short, the fix is to separate mutation from history control: let the Server Action do the secure server work, and let the client router handle replace semantics.