fix(dev): prevent HMR updates from being dropped during concurrent/slow network conditions#14953
fix(dev): prevent HMR updates from being dropped during concurrent/slow network conditions#14953NYCU-Chung wants to merge 2 commits intoremix-run:devfrom
Conversation
|
Hi @NYCU-Chung, Welcome, and thank you for contributing to React Router! Before we consider your pull request, we ask that you sign our Contributor License Agreement (CLA). We require this only once. You may review the CLA and sign it by adding your name to contributors.yml. Once the CLA is signed, the If you have already signed the CLA and received this response in error, or if you have any questions, please contact us at hello@remix.run. Thanks! - The Remix team |
|
Thank you for signing the Contributor License Agreement. Let's get this merged! 🥳 |
|
👋 We've moved away from Changesets to our own internal changes process. Please convert your changesets file to a change file in the proper package directory (i.e., |
…conditions The debounce utility used by enqueueUpdate was not async-safe: if a second HMR module update arrived while the first was still awaiting revalidation, the debounce would start a concurrent execution of the async handler. The first execution had already cleared routeUpdates, so the second execution found no pending route metadata and silently dropped the update. Two changes fix this: 1. Make debounce async-aware — if the wrapped function is already running, queue the call and replay it once the current run finishes (with the same delay). This prevents concurrent executions of the async handler. 2. Skip (instead of throw) routes whose module has not loaded yet. Under slow networks the react-router:hmr custom event (route metadata) arrives via WebSocket before the module finishes loading over HTTP. The old code threw when it encountered a route without a matching module. Now it skips that route and only clears entries that were actually processed, so unprocessed routes are picked up on the next debounced call. Together these changes ensure every HMR update is eventually applied regardless of network latency or rapid successive edits. Fixes remix-run#14906 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
f82caa7 to
56754c3
Compare
Summary
Fixes #14906
When the network is slow or edits arrive in rapid succession, HMR updates can be silently dropped or crash the dev server runtime. This PR fixes three race conditions in
refresh-utils.mjs:Race 1: Module not yet imported when
enqueueUpdatefiresVite sends the route manifest update (
react-router:hmrevent) and the module chunk as separate payloads. On slow networks, the manifest arrives and triggersenqueueUpdate, butwindow.__reactRouterRouteModuleUpdates.get(route.id)returnsundefinedbecause the module chunk hasn't finished downloading.Before: Throws
Error('[react-router:hmr] No module update found for route ...'), permanently breaking HMR for the session.After: Skips unready routes with
continueand retries them on the next cycle.Race 2:
routeUpdates.clear()wipes pending updatesThe old code called
routeUpdates.clear()andwindow.__reactRouterRouteModuleUpdates.clear()after processing, which also removed entries added by new edits that arrived during async processing.Before: New updates silently lost.
After: Only deletes successfully processed route IDs, preserving pending entries.
Race 3: Async-unaware debounce
The debounce wrapper uses
setTimeoutbutenqueueUpdateisasync. If a new edit triggers the debounce while the previousenqueueUpdateis still awaiting (e.g.,revalidate()), the new timeout can fire concurrently or the first execution's cleanup can race with the second's setup.Before: Fire-and-forget — no awareness of in-flight execution.
After: Tracks
runningstate; queues a re-execution after the current one completes.Reproduction
See the reproduction repo linked in #14906: https://github.com/AviVahl/react-router-hmr-issue
The issue reproduces consistently when simulating a slower network in the browser dev tools. With this fix applied, all consecutive HMR updates are correctly applied regardless of network speed.
Changes
packages/react-router-dev/vite/static/refresh-utils.mjs: 56 insertions, 26 deletionsrunning/queuedflagsTest plan
vite-hmr-hdr-test.ts"everything everywhere all at once" test validates concurrent edits still work