Link to the code that reproduces this issue
https://github.com/pigeon2gugu/redirect-test
Root Cause Hypothesis
When cacheComponents: true is enabled, a preserved (hidden) route instance retains its RedirectBoundary state after becoming inactive.
Because RedirectBoundary has no isActive guard, it can re-execute redirect side effects when the preserved instance is revisited during forward navigation — causing the same route stage to appear more than once before the flow moves
forward.
Description
With cacheComponents: true, a preserved hidden route appears able to replay redirect handling from RedirectBoundary and interfere with the next client-side forward navigation.
I created this repro to compare stock Next behavior against a small local RedirectBoundary patch.
The reproduction app has a small state machine:
/dashboard server-redirects based on cookie state
/verify-identity has a button that first prepares onboarding via a server action, then calls router.replace("/dashboard")
/onboarding has a button that first prepares complete via a server action, then calls router.replace("/dashboard")
/complete is the terminal page
To Reproduce
- Clone the repro and install dependencies
- Run
pnpm patch:off
- Run
pnpm dev
- Open
/dashboard
- Allow the app to redirect to
/verify-identity
- Click the button that prepares
onboarding and then calls router.replace("/dashboard")
- Observe whether
/verify-identity appears again before the app reaches /onboarding
- On
/onboarding, click the button that prepares complete and then calls router.replace("/dashboard")
- Observe whether
/onboarding appears again before the app reaches /complete
- Repeat the same comparison with
pnpm patch:on
Current vs. Expected behavior
Current behavior
With pnpm patch:off, I can reproduce duplicate visible participation of the same stage before the flow moves forward.
Observed sequence:
verify
verify
onboarding
onboarding
complete
Observed debug values:
This suggests that a preserved route is re-participating in redirect handling during forward navigation when it should no longer affect the active route.
Expected behavior
With stock behavior, I would expect the flow to move forward once per stage:
verify
onboarding
complete
When I apply the local patch (pnpm patch:on), that is exactly what happens.
The important distinction is that the patch does not remove Activity preservation or effect replay. It only prevents inactive preserved RedirectBoundary instances from executing redirect side effects again.
Local patch used for comparison
The local patch does four things:
- Passes
isActive from layout-router into RedirectBoundary
- Clears stale
redirect and redirectType when the boundary becomes inactive
- Only renders
HandleRedirect while the boundary is active
- Clears
redirectType during reset() as well
Local patch diff
diff --git a/node_modules/next/dist/client/components/layout-router.js b/node_modules/next/dist/client/components/layout-router.js
@@
- children: /*#__PURE__*/ (0, _jsxruntime.jsxs)(_redirectboundary.RedirectBoundary, {
+ children: /*#__PURE__*/ (0, _jsxruntime.jsxs)(_redirectboundary.RedirectBoundary, {
+ isActive: isActive && stateKey === activeStateKey,
children: [
/*#__PURE__*/ (0, _jsxruntime.jsx)(InnerLayoutRouter, {
url: url,
tree: tree,
params: params,
diff --git a/node_modules/next/dist/client/components/redirect-boundary.js b/node_modules/next/dist/client/components/redirect-boundary.js
@@
class RedirectErrorBoundary extends _react.default.Component {
@@
+ componentDidUpdate() {
+ if (!this.props.isActive && (this.state.redirect !== null || this.state.redirectType !== null)) {
+ this.setState({
+ redirect: null,
+ redirectType: null
+ });
+ }
+ }
render() {
const { redirect, redirectType } = this.state;
- if (redirect !== null && redirectType !== null) {
+ const shouldHandleRedirect = redirect !== null && redirectType !== null && this.props.isActive !== false;
+ if (shouldHandleRedirect) {
return /*#__PURE__*/ (0, _jsxruntime.jsx)(HandleRedirect, {
redirect: redirect,
redirectType: redirectType,
reset: ()=>this.setState({
- redirect: null
+ redirect: null,
+ redirectType: null
})
});
}
return this.props.children;
}
}
-function RedirectBoundary({ children }) {
+function RedirectBoundary({ children, isActive }) {
const router = (0, _navigation.useRouter)();
return /*#__PURE__*/ (0, _jsxruntime.jsx)(RedirectErrorBoundary, {
router: router,
+ isActive: isActive,
children: children
});
}
diff --git a/node_modules/next/dist/client/components/redirect-boundary.d.ts b/node_modules/next/dist/client/components/redirect-boundary.d.ts
@@
interface RedirectBoundaryProps {
router: AppRouterInstance;
children: React.ReactNode;
+ isActive?: boolean;
}
@@
+ componentDidUpdate(): void;
render(): React.ReactNode;
}
-export declare function RedirectBoundary({ children }: {
+export declare function RedirectBoundary({ children, isActive }: {
children: React.ReactNode;
+ isActive?: boolean;
}): import("react/jsx-runtime").JSX.Element;
Provide environment information
- Next.js stable repro version: `16.2.2`
- Next.js canary repro version: `16.2.1-canary.38`
- React: `19.2.4`
- Node: `20.14.0`
- pnpm: `10.6.5`
- OS: `macOS 14.6 (arm64)`
Which area(s) are affected? (Select all that apply)
Redirects
Which stage(s) are affected? (Select all that apply)
next build (local)
Additional context
I also reproduced the issue on stock canary.
- tested canary version:
16.2.1-canary.38
- result: the duplicated visible-stage behavior still reproduces with
pnpm patch:off
- note: I did not re-apply the local patch comparison on canary yet, because the current patch snapshots were prepared against
16.2.2
This repro demonstrates a consistent, reproducible A/B difference on both stable (16.2.2) and canary (16.2.1-canary.38), consistent with a hidden preserved RedirectBoundary being able to re-run redirect handling when it should no longer affect the active route.
Link to the code that reproduces this issue
https://github.com/pigeon2gugu/redirect-test
Root Cause Hypothesis
When
cacheComponents: trueis enabled, a preserved (hidden) route instance retains itsRedirectBoundarystate after becoming inactive.Because
RedirectBoundaryhas noisActiveguard, it can re-execute redirect side effects when the preserved instance is revisited during forward navigation — causing the same route stage to appear more than once before the flow movesforward.
Description
With
cacheComponents: true, a preserved hidden route appears able to replay redirect handling fromRedirectBoundaryand interfere with the next client-side forward navigation.I created this repro to compare stock Next behavior against a small local
RedirectBoundarypatch.The reproduction app has a small state machine:
/dashboardserver-redirects based on cookie state/verify-identityhas a button that first preparesonboardingvia a server action, then callsrouter.replace("/dashboard")/onboardinghas a button that first preparescompletevia a server action, then callsrouter.replace("/dashboard")/completeis the terminal pageTo Reproduce
pnpm patch:offpnpm dev/dashboard/verify-identityonboardingand then callsrouter.replace("/dashboard")/verify-identityappears again before the app reaches/onboarding/onboarding, click the button that preparescompleteand then callsrouter.replace("/dashboard")/onboardingappears again before the app reaches/completepnpm patch:onCurrent vs. Expected behavior
Current behavior
With
pnpm patch:off, I can reproduce duplicate visible participation of the same stage before the flow moves forward.Observed sequence:
verifyverifyonboardingonboardingcompleteObserved debug values:
This suggests that a preserved route is re-participating in redirect handling during forward navigation when it should no longer affect the active route.
Expected behavior
With stock behavior, I would expect the flow to move forward once per stage:
verifyonboardingcompleteWhen I apply the local patch (
pnpm patch:on), that is exactly what happens.The important distinction is that the patch does not remove
Activitypreservation or effect replay. It only prevents inactive preservedRedirectBoundaryinstances from executing redirect side effects again.Local patch used for comparison
The local patch does four things:
isActivefromlayout-routerintoRedirectBoundaryredirectandredirectTypewhen the boundary becomes inactiveHandleRedirectwhile the boundary is activeredirectTypeduringreset()as wellLocal patch diff
Provide environment information
Which area(s) are affected? (Select all that apply)
Redirects
Which stage(s) are affected? (Select all that apply)
next build (local)
Additional context
I also reproduced the issue on stock canary.
16.2.1-canary.38pnpm patch:off16.2.2This repro demonstrates a consistent, reproducible A/B difference on both stable (
16.2.2) and canary (16.2.1-canary.38), consistent with a hidden preservedRedirectBoundarybeing able to re-run redirect handling when it should no longer affect the active route.