AI & Agent Dev Bug Sandbox logo
AI & Agent Dev Bug Sandbox
Back to Radar

Router.Replace On IOS 17+ Chrome 127+ Causes History Entry Skip On Back Navigation

Using router.replace inside a Link's onClick handler before navigation causes the previous page to be skipped on back navigation on iOS 17+ and Chrome 127+ due to a WebKit history API race; workaround is to prevent default and sequence replace/push with a delay.

mediumConfidence 75%Next.jsAffected V13.xAffected V15.4.6

Origin Analysis

Next.js's router.replace uses history.replaceState, which on iOS 17+/Chrome 127+ (WebKit) can replace or remove the wrong history entry when called synchronously before a Link-initiated history.pushState, causing the previous page (PLP) to be dropped from the history stack.
1. Start at homepage. 2. Click 'PLP' page. 3. On PLP page, click 'PDP' page; the onClick handler calls router.replace to add a timestamp query param to the current PLP URL. 4. Click browser back; expected to return to PLP, but instead goes to homepage.

Fixing Code Block

import Link from 'next/link'; import { useRouter } from 'next/router'; export default function PLPPage() { const router = useRouter(); const handlePDPClick = (e) => { e.preventDefault(); // Replace current URL with timestamp query first router.replace( { pathname: router.pathname, query: { ...router.query, ts: Date.now() }, }, undefined, { shallow: true } ); // Delay the push to avoid WebKit history race setTimeout(() => { router.push('/pdp'); }, 0); }; return ( <Link href="/pdp" onClick={handlePDPClick}> Go to PDP </Link> ); }
The workaround prevents Link's default pushState from firing in the same event loop as the replaceState. By calling e.preventDefault(), we control the order. The router.replace updates the current PLP URL with the timestamp. A setTimeout with 0 delay ensures the replace operation is committed to the history before the subsequent pushState to /pdp, avoiding the WebKit race that deletes the previous entry. The shallow option prevents unnecessary data refetching.

Edge Case Audit

Risk: This workaround introduces a 0ms delay which may still be insufficient on slower devices; increase to 50-100ms or use requestAnimationFrame if issue persists. Calling preventDefault and manual router.push bypasses Next.js Link's built-in prefetching and accessibility features; ensure keyboard navigation still works. If the page unmounts before the timeout, the router.push may not execute; clear timeout on unmount. Rollback: delete the custom onClick and use normal <Link> if the timestamp query is not essential.

Ecosystem Topology