Repository navigation
Description
Which project does this relate to?
Router
Describe the bug
When a route is being preloaded (e.g. defaultPreload: 'intent') and its cached match is evicted while the preload's async work is still in flight — most commonly because the user clicks the link and actually navigates mid-preload, or because router.invalidate() / clearExpiredCache() runs — the preload pipeline crashes with an unhandled rejection:
TypeError: Cannot read properties of undefined (reading '_nonReactive')
at Re (Match-uwMccdg0.js:1:14474)
at async Promise.all (/index 1)
at async ze (Match-uwMccdg0.js:1:15299)
at async Ka.preloadRoute (Match-uwMccdg0.js:1:36895)
(minified production stack; the frames are load-matches.ts internals called from preloadRoute)
The root cause is that several code paths in packages/router-core/src/load-matches.ts re-look up the match with inner.router.getMatch(matchId) after an await and read ._nonReactive without guarding against the match having been removed from state.matches / pendingMatches / cachedMatches in the meantime, e.g. the non-null assertions here (current main):
preBeforeLoadSetup:const existingMatch = inner.router.getMatch(matchId)!followed byexistingMatch._nonReactive.beforeLoadPromiseexecuteBeforeLoad:const match = inner.router.getMatch(matchId)!followed bymatch._nonReactive.loadPromise- similar unguarded reads in the loader-run/cleanup paths
For a preload, the match only lives in cachedMatches, so any eviction during the in-flight async work (lazy chunk load, beforeLoad, loader) makes the next getMatch(matchId) return undefined and the next ._nonReactive access throw.
Navigation itself still succeeds (the real navigation builds its own matches), so the impact is an unhandled rejection that pollutes the console and error tracking (Sentry etc.) on every hover-preload-then-click race.
Steps to reproduce
- Create a router with
defaultPreload: 'intent'and a route whose loader (or lazy component chunk) takes a moment to resolve. - Hover a
<Link>to that route sopreloadRoutestarts. - Before the preload settles, click the link (or call
router.invalidate()). - Observe the unhandled
TypeError: Cannot read properties of undefined (reading '_nonReactive')in the console.
The regression tests added in #7003 / #7006 (clearExpiredCache() / invalidate() while a preloadRoute loader is unresolved) reproduce the same race deterministically.
Expected behavior
An in-flight preload whose match has been evicted should clean up quietly (resolve/clear its controlled promises and pending timeout) instead of throwing an unhandled TypeError.
Your Example Website or App
Reproducible in production on x.com (hover the login link on the logged-out homepage, then click it while the preload is in flight).
Platform
- OS: macOS
- Browser: Chrome
- Version: @tanstack/react-router 1.170.17 / @tanstack/router-core 1.171.14 (unguarded reads still present on
main)
Additional context
There are two open PRs that appear to address exactly this: #7003 and #7006. Filing this issue since neither PR links to a tracking issue and the crash is otherwise undocumented (closest existing report is #7753, which is a different symptom of a cleared loadPromise).
Activity
fix(deps): drop router-core preload-eviction patch, now fixed upstream
fix(deps): drop router-core preload-eviction patch, now fixed upstream
fix(deps): @tanstack/react-router 1.170.39 — in-flight preload of an …
fix(router): a link tapped mid-preload no longer throws match._nonRea…
fix(router): a link tapped mid-preload no longer throws match._nonRea…
fix(router): a link tapped mid-preload no longer throws match._nonRea…
fix(router): stop a link tapped mid-preload throwing match._nonReacti…
fix(router): a link tapped mid-preload no longer throws match._nonRea…
fix(router): a link tapped mid-preload no longer throws match._nonRea…
fix(router): a link tapped mid-preload no longer throws match._nonRea…
test(platform): pin the router-core patch on the awaited-loader path
test(platform): pin the router-core patch on the awaited-loader path