<Link> click handling freezes the page under rapid repeated navigation (react-router-dom v7.18.3)
Reproduction
Reproduction
Minimal reproduction repo: https://github.com/Duduceretta/react-router-link-freeze-repro (mirrors the nested createBrowserRouter guard→layout→page structure and the persistent sidebar <Link> items described below; README has exact repro steps).
Steps that reproduce reliably in the full app:
react-router-dom:7.18.3react/react-dom:19.x- App structure:
createBrowserRouterwith nested routes —ProtectedRoute(auth guard usinguseLocation()+Navigate) → layout route (persistent sidebar +<Outlet/>, itself usinguseLocation()and auseEffectkeyed onlocation.pathname) → leaf page routes. - Sidebar renders several
<Link to={itemPath}>items; each real mouse click on a different item navigates to a different leaf route. - Click real, physical mouse clicks on 2+ different sidebar
<Link>items back-to-back, as fast as physically possible, for ~10-20 clicks. The tab becomes unresponsive to further input (clicks, and even opening/using DevTools on that tab) for many seconds.
What did NOT reproduce it
- A flat
createBrowserRouterwith 2 routes and no nested layout/guard, navigating viarouter.navigate()in a plain<button onClick>(no<Link>involved) — clicked as fast as physically possible, never froze. - Removing every custom side-effect layer we had (a SignalR listener, several
useEffects, a custom ripple effect, various context providers) while still using<Link>for navigation — still froze. - Programmatic/synthetic clicks (
element.click(), CDP-driven automation) never reproduced this at all, even at a much higher rate than physically possible with a mouse. Only real, physical rapid mouse clicks reproduce it.
What DID resolve it
Replacing <Link to={item.path}> with:
const navigate = useNavigate();
<button type="button" onClick={() => { if (!isActive) navigate(item.path); }}>
...
</button>
No other change. Same routes, same layout, same guards. This alone made the freeze stop occurring, tested with 30+ real rapid clicks across every route, repeatedly.
We also tried <RouterProvider useTransitions={false}>, which measurably reduced (but did not eliminate) the accumulated work — only combined with the <Link> → <button> swap did the freeze fully disappear.
System Info
## System Info
System:
OS: Windows 11 10.0.22631
CPU: (12) x64 12th Gen Intel(R) Core(TM) i5-12450H
Memory: 2.91 GB / 15.71 GB
Binaries:
Node: 24.16.0
npm: 11.17.0
Browsers:
Edge: Chromium (152.0.4191.53)
npmPackages:
react: 19.2.8
react-dom: 19.2.8
react-router-dom: 7.18.3
vite: 8.2.2
Used Package Manager
npm
Expected Behavior
Expected Behavior
Rapid repeated clicks on different <Link> targets should be coalesced/interrupted the same way rapid navigate() calls apparently are — not accumulate into a backlog that takes over a minute (relative to the ~10s input burst) to drain, and never freeze the tab's responsiveness to unrelated input (scrolling kept working throughout; only click handling and page reactivity stalled).
Actual Behavior
Actual Behavior
Clicking a <Link to={path}> repeatedly and rapidly (dozens of clicks in a few seconds, real mouse clicks — not synthetic/programmatic ones) causes the entire tab to stop responding to input for many seconds, with no console error, no unhandled rejection, and low CPU usage on the affected thread. The page eventually recovers on its own after ~15-25s without any further interaction.
Performance trace findings
We captured a ~30s Chrome Performance trace spanning the freeze. Key findings:
- CPU usage stayed low throughout (confirmed via both the trace and the OS Task Manager) — this is a single-thread saturation issue, not a busy CPU-bound loop, which is why total system CPU looked unremarkable on a multi-core machine.
- The renderer main thread had no idle gaps larger than ~800ms across the entire 30s window — it was continuously processing work.
- We stopped clicking at ~9.6s into the recording (36 real clicks total), but
RunTaskevents on the main thread continued at a high, roughly constant rate (150-250 tasks per 500ms bucket) for another ~16 seconds after input stopped, only tapering off around the 25.5s mark. - Interestingly, opening/using Chrome DevTools' "inspect element" tool against the frozen tab also failed to respond during the freeze (it depends on the same main thread), even though
document.elementFromPoint()executed from an already-open Console (attached before the freeze) returned correctly and1+1evaluated fine in that Console — so the JS engine itself wasn't deadlocked; it was continuously busy processing a backlog that outlived the input by more than a minute of wall-clock ratio to the input window.
We don't have a minimal reproduction narrowed down to a single React Router internal yet (we found this via progressive app-level isolation, not by reading the router's source), but the shape of the evidence (continuous, non-blocking-looking work that vastly outlives the triggering input, present with <Link> and absent with an equivalent navigate() call in a plain event handler) points at something in <Link>'s click handling / transition scheduling accumulating faster than it can drain under a burst of rapid real clicks.
Additional context
Happy to share the full captured trace JSON (~55MB uncompressed) and a minimal repro repo if useful — let us know the preferred way to share a large trace file on this tracker.
Source: remix-run/react-router