Skip to content

fix: match loading#7637

Open
schiller-manuel wants to merge 13 commits into
mainfrom
fix-match-loading
Open

fix: match loading#7637
schiller-manuel wants to merge 13 commits into
mainfrom
fix-match-loading

Conversation

@schiller-manuel

@schiller-manuel schiller-manuel commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

fixes #7602
fixes #7120
fixes #7457
fixes #7635

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed propagation of beforeLoad context values into sub-routes during background reloads, including when beforeLoad throws.
    • Prevented stale loading states from appearing during redirects and background navigation.
    • Improved pending UI behavior when redirects occur with view transitions enabled.
  • Documentation

    • Updated RouteMatch.status lifecycle docs to remove the redirected state.
  • Tests

    • Added Playwright regression coverage for prior redirect/pending issues.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Removes the 'redirected' RouteMatch.status variant from all packages and rewrites the match lifecycle in load-matches.ts and router.ts to use generation-guarded context commits, background loader detachment, and cache eviction with promise settlement. Two new Playwright e2e fixtures reproduce beforeLoad redirect race conditions (issues 7120 and 7457).

Changes

Core router lifecycle and framework alignment

Layer / File(s) Summary
RouteMatch status contract and adapter rendering updates
packages/router-core/src/Matches.ts, docs/router/api/router/RouteMatchType.md, .changeset/violet-poets-wait.md, packages/router-devtools-core/src/useStyles.tsx, packages/router-devtools-core/src/utils.tsx, packages/react-router/src/Match.tsx, packages/solid-router/src/Match.tsx, packages/vue-router/src/Match.tsx
RouteMatch.status union drops 'redirected'; docs and changeset updated; devtools color maps trimmed; React, Solid, and Vue MatchInner components remove the redirected render branch and associated promise helpers; React getMatchPromise adds router.latestLoadPromise fallback.
Controlled promise settle-once guard and server-gated redirect headers
packages/router-core/src/utils.ts, packages/router-core/src/redirect.ts, skills/bundle-size-optimization/SKILL.md
createControlledPromise resolve/reject now guard on status === 'pending' to prevent double-settlement; redirect() gates Location header injection with isServer check via a new getRedirectHeaders helper; bundle-size docs add isServer DCE guidance.
load-matches: context commit/patch, redirect-notFound routing, loader generation tracking
packages/router-core/src/load-matches.ts
loadMatches adds preloadMatchIds targeting; commitMatch/patchMatch split full vs lightweight state updates; clearMatchPromises exported; redirect/notFound handling centralized in shared helpers with promise settlement; executeBeforeLoad gains no-beforeLoad fast path and isCurrentBeforeLoad generation guard; handleBeforeLoad reorganized into queued execution; loader phase tracks loaderPromise identity and generation; runLoaderPhase detaches stale-while-revalidate background reloads; notFound boundary uses commitMatch with root globalNotFound special-casing; head execution inlined with Promise.all; loadRouteChunk/preloadRouteComponents simplified.
router.ts: load rewrite, cache eviction, preload promotion, buildMiddlewareChain
packages/router-core/src/router.ts
load() rewritten with controlled loadPromise coordinated with startTransition/startViewTransition; updateMatch evicts cached entries on non-success/non-pending; clearExpiredCache treats error as immediately non-cacheable; preload-to-active promotion creates a fresh controlled loadPromise; buildLocation switches from applySearchMiddleware to buildMiddlewareChain; preloadRoute passes preloadMatchIds and guards updateMatch for already-active matches; getParentContext prioritizes parentMatch.context; basepath/rewrite changes update stores.location immediately; resolveRedirect performs unsafe-protocol checks.
SSR hydration pending cleanup, Vue Transitioner typing, view-transition mock
packages/router-core/src/ssr/ssr-client.ts, packages/vue-router/src/Transitioner.tsx, e2e/vue-router/view-transitions/tests/view-transitions.spec.ts
SSR hydration clears displayPendingPromise via direct mutation before batched updateMatch; Vue Transitioner tightens originalStartViewTransition type to Promise<void>-returning; Vue e2e view-transition mock returns both finished and updateCallbackDone as the same resolved promise.
router-core: load, hydrate, redirect-notFound, generation isolation, loadRouteChunk tests
packages/router-core/tests/load.test.ts, packages/router-core/tests/hydrate.test.ts
Adds server/client redirect Location header behavior, beforeLoad/loader redirect-notFound parameterized cases in direct and preload paths, stale reload cache eviction and promise settlement, pending timeout re-arm and late-resolution ignoring with AbortError, beforeLoad context lifecycle clearing, loadRouteChunk partial preload deduping and concurrency, generation overlap and isolation; adds displayPendingPromise setup/teardown coverage on resolve and reject paths.
React: routeContext background reload, notFound context, store-update counts
packages/react-router/tests/routeContext.test.tsx, packages/react-router/tests/not-found.test.tsx, packages/react-router/tests/store-updates-during-navigation.test.tsx
routeContext.test.tsx adds beforeLoad context propagation tests across background reloads, re-entry during in-flight reloads, aborted reload survival, error component context access, and blocking reload atomicity; not-found.test.tsx adds notFound routeId targeting and context clearing after invalidation; store-update count adjusted from 8 to 7.
Solid and Vue: routeContext, store-update counts, preload cache eviction assertions
packages/solid-router/tests/routeContext.test.tsx, packages/solid-router/tests/store-updates-during-navigation.test.tsx, packages/vue-router/tests/store-updates-during-navigation.test.tsx
Solid adds background-reload context preservation test; Solid and Vue reduce expected store update counts for async loader/pendingMs and notFound-in-beforeLoad scenarios; both add cachedMatches assertions confirming /posts is absent after preload redirects; Vue adjusts counts for hover preload and preloaded navigation cases.

New React e2e reproductions for issues 7120 and 7457

Layer / File(s) Summary
Issue 7120 e2e fixture
e2e/react-router/issue-7120/*
Adds root route with pendingMs: 0, async beforeLoad redirecting to /posts after ~1s (guarded by matches), pending component setting globalThis.__pendingSeen, lazy posts route with async loader, router with defaultViewTransition: true, and Playwright spec validating redirect, pending flag, cleared pending element, and zero page/console errors.
Issue 7457 e2e fixture
e2e/react-router/issue-7457/*
Adds root route with 1.5s async beforeLoad, index route unconditionally redirecting to /another, default pending component marking globalThis.__pendingSeen, auto-generated route tree, and Playwright spec validating redirect to /another, pending component display and clearance, and zero page/console errors.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant RouterLoad as router.load()
  participant LoadMatches as loadMatches
  participant HandleBeforeLoad as handleBeforeLoad
  participant ExecuteBeforeLoad as executeBeforeLoad
  participant RunLoaderPhase as runLoaderPhase
  participant Cache as router cache

  Browser->>RouterLoad: navigate to route
  RouterLoad->>LoadMatches: matches + preloadMatchIds
  LoadMatches->>HandleBeforeLoad: queueExecution (checks in-flight promises)
  HandleBeforeLoad->>ExecuteBeforeLoad: run if generation current
  ExecuteBeforeLoad->>LoadMatches: commitMatch(__beforeLoadContext)
  LoadMatches->>RunLoaderPhase: loader with generation tracking
  alt background reload
    RunLoaderPhase->>LoadMatches: detach (clear invalid, return)
  else normal load
    RunLoaderPhase->>LoadMatches: await loader, commitMatch success
  end
  alt redirect or notFound
    LoadMatches->>Cache: clearMatchPromises, evict entry
    LoadMatches->>RouterLoad: throw redirect/notFound
  end
  RouterLoad->>Browser: settled load with status code
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • TanStack/router#7584: Modifies packages/solid-router/src/Match.tsx around pending/redirected match state resolution, directly overlapping with the redirected branch removal in this PR.
  • TanStack/router#7416: Modifies ssr-client.ts hydration behavior (hydrate() call ordering), which shares the same function as the displayPendingPromise cleanup adjusted in this PR.
  • TanStack/router#6867: Adjusts loader/match lifecycle code in load-matches.ts and router.ts around stale-loader re-execution rules, with overlapping changes to the same lifecycle refactored in this PR.

Suggested reviewers

  • Sheraff
  • tannerlinsley

🐇 Hops through the match tree, no redirect in sight,
The 'redirected' status has vanished from light.
commitMatch guards context with each generation's run,
Stale loaders detach before old promises are done.
The pending flag waves, then disappears with grace —
A cleaner route lifecycle has taken its place! 🌿

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'fix: match loading' is vague and generic, not clearly summarizing the specific changes or main objectives addressed in this comprehensive changeset. Use a more descriptive title that captures the main fix, such as 'fix: context propagation and redirect handling in route matching' or similar.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses all three linked issues: context propagation in sub-route loading (#7602), redirect handling in root beforeLoad (#7120), and race condition in match rendering (#7457). Code changes include removing 'redirected' status, fixing context propagation, improving redirect/notFound handling, and refactoring match lifecycle.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the three linked issues: match loading, context propagation, redirect handling, and race conditions. Supporting changes include test cases for new E2E test fixtures and documentation updates for the API change.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-match-loading

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud

nx-cloud Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 1d394e1

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 11m 31s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2m View ↗

☁️ Nx Cloud last updated this comment at 2026-06-17 19:22:06 UTC

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

1 package(s) bumped directly, 22 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/router-core 1.171.13 → 1.171.14 Changeset
@tanstack/react-router 1.170.16 → 1.170.17 Dependent
@tanstack/react-start 1.168.26 → 1.168.27 Dependent
@tanstack/react-start-client 1.168.14 → 1.168.15 Dependent
@tanstack/react-start-rsc 0.1.25 → 0.1.26 Dependent
@tanstack/react-start-server 1.167.20 → 1.167.21 Dependent
@tanstack/router-cli 1.167.17 → 1.167.18 Dependent
@tanstack/router-generator 1.167.17 → 1.167.18 Dependent
@tanstack/router-plugin 1.168.18 → 1.168.19 Dependent
@tanstack/router-vite-plugin 1.167.18 → 1.167.19 Dependent
@tanstack/solid-router 1.170.16 → 1.170.17 Dependent
@tanstack/solid-start 1.168.26 → 1.168.27 Dependent
@tanstack/solid-start-client 1.168.14 → 1.168.15 Dependent
@tanstack/solid-start-server 1.167.20 → 1.167.21 Dependent
@tanstack/start-client-core 1.170.12 → 1.170.13 Dependent
@tanstack/start-plugin-core 1.171.18 → 1.171.19 Dependent
@tanstack/start-server-core 1.169.15 → 1.169.16 Dependent
@tanstack/start-static-server-functions 1.167.17 → 1.167.18 Dependent
@tanstack/start-storage-context 1.167.15 → 1.167.16 Dependent
@tanstack/vue-router 1.170.15 → 1.170.16 Dependent
@tanstack/vue-start 1.168.25 → 1.168.26 Dependent
@tanstack/vue-start-client 1.167.17 → 1.167.18 Dependent
@tanstack/vue-start-server 1.167.20 → 1.167.21 Dependent

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: 3912c6aeae02
  • Measured at: 2026-06-17T19:11:29.567Z
  • Baseline source: history:f23ed0fe737b
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Initial gzip Raw Brotli Trend
react-router.minimal 87.21 KiB -119 B (-0.13%) 87.07 KiB 273.37 KiB 75.83 KiB ▄▆▆▆▆▆▆▆███▁
react-router.full 90.94 KiB -123 B (-0.13%) 90.80 KiB 285.27 KiB 79.05 KiB ▃▅▅▅▅▅▅▅███▁
solid-router.minimal 35.39 KiB -140 B (-0.38%) 35.26 KiB 105.41 KiB 31.95 KiB ▆▆▆▆▆▆▆▆███▁
solid-router.full 40.46 KiB -126 B (-0.30%) 40.33 KiB 120.63 KiB 36.39 KiB ▅▅▅▅▅▅▅▅███▁
vue-router.minimal 52.91 KiB -100 B (-0.18%) 52.78 KiB 149.49 KiB 47.60 KiB ▅▅▅▅▅▅▅▅███▁
vue-router.full 58.87 KiB -127 B (-0.21%) 58.74 KiB 168.25 KiB 52.71 KiB ▆▆▆▆▆▆▆▆███▁
react-start.minimal 101.86 KiB -95 B (-0.09%) 101.72 KiB 321.71 KiB 88.24 KiB ▃▅▅▅▅▅▅▅███▁
react-start.deferred-hydration 102.60 KiB -98 B (-0.09%) 101.75 KiB 323.08 KiB 88.91 KiB ▂▅▅▅▅▅▅▅███▁
react-start.full 105.26 KiB -103 B (-0.10%) 105.12 KiB 331.64 KiB 91.12 KiB ▂▅▅▅▅▅▅▅███▁
react-start.rsbuild.minimal 99.66 KiB +13 B (+0.01%) 99.49 KiB 316.34 KiB 85.73 KiB ▁▅▅▅▅▅▅▅█▆▆▇
react-start.rsbuild.minimal-iife 100.06 KiB +10 B (+0.01%) 99.90 KiB 317.27 KiB 86.20 KiB ▁▅▅▅▅▅▅▅█▇▇█
react-start.rsbuild.full 102.87 KiB -8 B (-0.01%) 102.70 KiB 326.39 KiB 88.51 KiB ▁▅▅▅▅▅▅▅█▇▇▆
solid-start.minimal 49.51 KiB -135 B (-0.27%) 49.38 KiB 151.49 KiB 43.69 KiB ▆▆▆▆▆▆▆▆███▁
solid-start.deferred-hydration 52.77 KiB -134 B (-0.25%) 49.43 KiB 159.53 KiB 46.76 KiB ▆▆▆▆▆▆▆▆███▁
solid-start.full 55.35 KiB -100 B (-0.18%) 55.22 KiB 168.54 KiB 48.87 KiB ▅▅▅▅▅▅▅▅███▁
vue-start.minimal 70.92 KiB -155 B (-0.21%) 70.79 KiB 206.64 KiB 62.77 KiB ▆▆▆▆▆▆▆▆███▁
vue-start.full 74.89 KiB -172 B (-0.22%) 74.76 KiB 219.28 KiB 66.32 KiB ▆▆▆▆▆▆▆▆███▁

Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better.

@pkg-pr-new

pkg-pr-new Bot commented Jun 15, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7637

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7637

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7637

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7637

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7637

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7637

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7637

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7637

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7637

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7637

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7637

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7637

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7637

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7637

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7637

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7637

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7637

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7637

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7637

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7637

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7637

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7637

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7637

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7637

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7637

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7637

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7637

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7637

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7637

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7637

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7637

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7637

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7637

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7637

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7637

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7637

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7637

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7637

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7637

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7637

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7637

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7637

commit: 0d36b6c

@codspeed-hq

codspeed-hq Bot commented Jun 15, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 23.73%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 11 improved benchmarks
❌ 10 regressed benchmarks
✅ 86 untouched benchmarks
⏩ 37 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory mem loader-data-retention (solid) 158.4 KB 2,333 KB -93.21%
Memory mem loader-data-retention (react) 203.5 KB 2,335.7 KB -91.29%
Memory mem navigation-churn (react) 430.2 KB 1,188.3 KB -63.8%
Memory mem unique-location-churn (react) 728.8 KB 1,177.4 KB -38.1%
Memory mem interrupted-navigations (react) 759.7 KB 1,210.4 KB -37.24%
Memory mem serialization-payload (react) 31.2 MB 34.9 MB -10.56%
Memory mem request-churn (react) 1.6 MB 1.7 MB -7%
Simulation ssr redirect (react) 61.4 ms 64.8 ms -5.3%
Simulation ssr redirect (solid) 59.5 ms 62.1 ms -4.22%
Memory mem request-churn (vue) 1.3 MB 1.3 MB -3.91%
Memory mem preload-churn (solid) 578.1 KB 246.4 KB ×2.3
Memory mem preload-churn (react) 610.3 KB 436.6 KB +39.78%
Memory mem aborted-requests (solid) 1.8 MB 1.6 MB +12.93%
Memory mem error-paths unmatched (react) 1.1 MB 1 MB +7.83%
Simulation ssr control-flow route headers (solid) 82.5 ms 76.8 ms +7.41%
Simulation ssr selective (solid) 74.8 ms 70.3 ms +6.4%
Simulation ssr server-fn multipart (solid) 63 ms 59.9 ms +5.07%
Simulation ssr server-fn raw-stream (solid) 111.8 ms 106.6 ms +4.88%
Memory mem mount-unmount (solid) 493 KB 472.4 KB +4.36%
Simulation ssr server-fn raw-response (solid) 57.2 ms 55.3 ms +3.54%
... ... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fix-match-loading (0d36b6c) with main (f23ed0f)

Open in CodSpeed

Footnotes

  1. 37 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
e2e/react-router/issue-7120/src/main.tsx (1)

22-22: Add typed global declaration for __pendingSeen to replace (globalThis as any) casts.

Replace the untyped as any pattern across three files with a global type declaration:

  • e2e/react-router/issue-7120/src/main.tsx Line 22
  • e2e/react-router/issue-7120/tests/issue-7120.repro.spec.ts Line 23
  • e2e/react-router/issue-7457/src/main.tsx Line 19

This violates the TypeScript strict mode guideline for these .ts/.tsx files.

Add to a global d.ts file (e.g., vite-env.d.ts)
declare global {
  var __pendingSeen: boolean | undefined
}
export {}

Then update all occurrences from (globalThis as any).__pendingSeen to globalThis.__pendingSeen.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/react-router/issue-7120/src/main.tsx` at line 22, Add a typed global
declaration for __pendingSeen to eliminate the untyped `as any` casts. Create or
update a global type declaration file (e.g., vite-env.d.ts) with a declare
global block that types __pendingSeen as `boolean | undefined`, then export an
empty object. Replace all three occurrences of `(globalThis as
any).__pendingSeen` with `globalThis.__pendingSeen` in:
e2e/react-router/issue-7120/src/main.tsx at line 22,
e2e/react-router/issue-7120/tests/issue-7120.repro.spec.ts at line 23, and
e2e/react-router/issue-7457/src/main.tsx at line 19. This will satisfy
TypeScript strict mode requirements without the unsafe `any` casts.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/react-router/issue-7120/package.json`:
- Around line 14-20: The package.json file uses workspace:^ for internal
workspace packages (`@tanstack/react-router` and `@tanstack/router-e2e-utils`) but
the repository guideline requires workspace:* for all internal dependencies.
Change both `@tanstack/react-router` and `@tanstack/router-e2e-utils` entries from
workspace:^ to workspace:* to align with the established guideline and prevent
dependency drift.

In `@e2e/react-router/issue-7120/tsconfig.json`:
- Line 12: The types configuration in tsconfig.json at the "types" field
currently only includes vite/client, which does not provide Node global type
definitions. Since playwright.config.ts uses process.env, the Node types must be
added to the types array to resolve the process global and satisfy type-checking
when tsc --noEmit is run. Add "node" to the types array alongside the existing
"vite/client" entry.

In `@e2e/react-router/issue-7457/package.json`:
- Around line 14-15: Replace the version specifiers for internal dependencies in
the package.json file from workspace:^ to workspace:* to comply with the coding
guideline for internal dependency versioning. Specifically, change the version
specifier for `@tanstack/react-router` on line 14, `@tanstack/router-plugin` on line
15, and the internal dependency on line 21 from workspace:^ to workspace:*.

In `@packages/solid-router/tests/routeContext.test.tsx`:
- Around line 2479-2487: The `sawUndefinedContext` variable uses a one-time
assignment during component setup, so it will not detect if the context becomes
undefined after the component mounts. Replace the simple boolean assignment with
a reactive pattern: create a signal to store the `sawUndefinedContext` state and
use `createEffect` to reactively update it whenever `number()` changes, ensuring
that any context loss during the component's lifetime (such as during a
background loader reload) is properly detected and reflected in the rendered
output.

---

Nitpick comments:
In `@e2e/react-router/issue-7120/src/main.tsx`:
- Line 22: Add a typed global declaration for __pendingSeen to eliminate the
untyped `as any` casts. Create or update a global type declaration file (e.g.,
vite-env.d.ts) with a declare global block that types __pendingSeen as `boolean
| undefined`, then export an empty object. Replace all three occurrences of
`(globalThis as any).__pendingSeen` with `globalThis.__pendingSeen` in:
e2e/react-router/issue-7120/src/main.tsx at line 22,
e2e/react-router/issue-7120/tests/issue-7120.repro.spec.ts at line 23, and
e2e/react-router/issue-7457/src/main.tsx at line 19. This will satisfy
TypeScript strict mode requirements without the unsafe `any` casts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31051461-bf2e-4485-99b1-ad9e397b0087

📥 Commits

Reviewing files that changed from the base of the PR and between a415471 and 9698aee.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (46)
  • .changeset/violet-poets-wait.md
  • docs/router/api/router/RouteMatchType.md
  • e2e/react-router/issue-7120/index.html
  • e2e/react-router/issue-7120/package.json
  • e2e/react-router/issue-7120/playwright.config.ts
  • e2e/react-router/issue-7120/src/main.tsx
  • e2e/react-router/issue-7120/src/posts.lazy.tsx
  • e2e/react-router/issue-7120/tests/issue-7120.repro.spec.ts
  • e2e/react-router/issue-7120/tsconfig.json
  • e2e/react-router/issue-7120/vite.config.js
  • e2e/react-router/issue-7457/index.html
  • e2e/react-router/issue-7457/package.json
  • e2e/react-router/issue-7457/playwright.config.ts
  • e2e/react-router/issue-7457/src/main.tsx
  • e2e/react-router/issue-7457/src/routeTree.gen.ts
  • e2e/react-router/issue-7457/src/routes/__root.tsx
  • e2e/react-router/issue-7457/src/routes/another.tsx
  • e2e/react-router/issue-7457/src/routes/index.tsx
  • e2e/react-router/issue-7457/src/vite-env.d.ts
  • e2e/react-router/issue-7457/tests/issue-7457.repro.spec.ts
  • e2e/react-router/issue-7457/tsconfig.json
  • e2e/react-router/issue-7457/vite.config.js
  • e2e/vue-router/view-transitions/tests/view-transitions.spec.ts
  • packages/react-router/src/Match.tsx
  • packages/react-router/tests/not-found.test.tsx
  • packages/react-router/tests/redirect.test.tsx
  • packages/react-router/tests/routeContext.test.tsx
  • packages/router-core/src/Matches.ts
  • packages/router-core/src/load-matches.ts
  • packages/router-core/src/redirect.ts
  • packages/router-core/src/router.ts
  • packages/router-core/src/ssr/ssr-client.ts
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/hydrate.test.ts
  • packages/router-core/tests/load.test.ts
  • packages/router-devtools-core/src/useStyles.tsx
  • packages/router-devtools-core/src/utils.tsx
  • packages/solid-router/src/Match.tsx
  • packages/solid-router/tests/redirect.test.tsx
  • packages/solid-router/tests/routeContext.test.tsx
  • packages/solid-router/tests/store-updates-during-navigation.test.tsx
  • packages/vue-router/src/Match.tsx
  • packages/vue-router/src/Transitioner.tsx
  • packages/vue-router/tests/redirect.test.tsx
  • packages/vue-router/tests/store-updates-during-navigation.test.tsx
  • skills/bundle-size-optimization/SKILL.md
💤 Files with no reviewable changes (5)
  • packages/react-router/tests/redirect.test.tsx
  • packages/solid-router/tests/redirect.test.tsx
  • packages/router-devtools-core/src/utils.tsx
  • packages/vue-router/tests/redirect.test.tsx
  • packages/vue-router/src/Match.tsx

Comment on lines +14 to +20
"@tanstack/react-router": "workspace:^",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align internal workspace dependency ranges with repo guideline.

Line 14 and Line 20 use workspace:^ for internal packages, but this PR’s package guideline requires workspace:* in package.json files. Please switch both entries (or explicitly document an e2e exception in the guideline to avoid drift).

Suggested patch
-    "`@tanstack/react-router`": "workspace:^",
+    "`@tanstack/react-router`": "workspace:*",
@@
-    "`@tanstack/router-e2e-utils`": "workspace:^",
+    "`@tanstack/router-e2e-utils`": "workspace:*",

As per coding guidelines, internal dependencies in **/package.json must use the workspace protocol workspace:*.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@tanstack/react-router": "workspace:^",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@tanstack/router-e2e-utils": "workspace:^",
"`@tanstack/react-router`": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"`@playwright/test`": "^1.50.1",
"`@tanstack/router-e2e-utils`": "workspace:*",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/react-router/issue-7120/package.json` around lines 14 - 20, The
package.json file uses workspace:^ for internal workspace packages
(`@tanstack/react-router` and `@tanstack/router-e2e-utils`) but the repository
guideline requires workspace:* for all internal dependencies. Change both
`@tanstack/react-router` and `@tanstack/router-e2e-utils` entries from workspace:^
to workspace:* to align with the established guideline and prevent dependency
drift.

Source: Coding guidelines

"resolveJsonModule": true,
"allowJs": true,
"skipLibCheck": true,
"types": ["vite/client"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include Node types for Playwright config type-checking.

Line 12 restricts global types to vite/client, but playwright.config.ts uses process.env (Line 18 there). With tsc --noEmit in Line 8 of package.json, this can fail type-checking due to missing Node globals.

Suggested patch
-    "types": ["vite/client"]
+    "types": ["vite/client", "node"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"types": ["vite/client"]
"types": ["vite/client", "node"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/react-router/issue-7120/tsconfig.json` at line 12, The types
configuration in tsconfig.json at the "types" field currently only includes
vite/client, which does not provide Node global type definitions. Since
playwright.config.ts uses process.env, the Node types must be added to the types
array to resolve the process global and satisfy type-checking when tsc --noEmit
is run. Add "node" to the types array alongside the existing "vite/client"
entry.

Comment on lines +14 to +15
"@tanstack/react-router": "workspace:^",
"@tanstack/router-plugin": "workspace:^",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify workspace protocol usage in this package
rg -n '"`@tanstack/`(react-router|router-plugin|router-e2e-utils)"\s*:\s*"workspace:\^"' \
  e2e/react-router/issue-7457/package.json

Repository: TanStack/router

Length of output: 208


Use workspace:* for internal dependencies in this package.

Lines 14–15 and 21 use workspace:^, but the coding guideline for **/package.json requires workspace:* for internal dependencies.

Suggested patch
-    "`@tanstack/react-router`": "workspace:^",
-    "`@tanstack/router-plugin`": "workspace:^",
+    "`@tanstack/react-router`": "workspace:*",
+    "`@tanstack/router-plugin`": "workspace:*",
@@
-    "`@tanstack/router-e2e-utils`": "workspace:^",
+    "`@tanstack/router-e2e-utils`": "workspace:*",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/react-router/issue-7457/package.json` around lines 14 - 15, Replace the
version specifiers for internal dependencies in the package.json file from
workspace:^ to workspace:* to comply with the coding guideline for internal
dependency versioning. Specifically, change the version specifier for
`@tanstack/react-router` on line 14, `@tanstack/router-plugin` on line 15, and the
internal dependency on line 21 from workspace:^ to workspace:*.

Source: Coding guidelines

Comment on lines +2479 to +2487
component: () => {
const context = reloadInFlightIndexRoute.useRouteContext()
const number = () => context().number
sawUndefinedContext ||= number() === undefined

return (
<div>
number = {String(number())}, saw undefined ={' '}
{String(sawUndefinedContext)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and read the relevant section
fd -t f "routeContext.test.tsx" 2>/dev/null | head -5

Repository: TanStack/router

Length of output: 207


🏁 Script executed:

# Read the specific lines mentioned in the review
cat -n packages/solid-router/tests/routeContext.test.tsx | sed -n '2450,2530p'

Repository: TanStack/router

Length of output: 3024


🏁 Script executed:

# Check the file for imports to see what's currently imported
head -50 packages/solid-router/tests/routeContext.test.tsx

Repository: TanStack/router

Length of output: 1240


🏁 Script executed:

# Check the imports for Solid.js functions to see what's available
rg "import.*from.*solid-js" packages/solid-router/tests/routeContext.test.tsx

Repository: TanStack/router

Length of output: 108


🏁 Script executed:

# Search for other uses of createSignal or createEffect in the test file to understand patterns used
rg "createSignal|createEffect" packages/solid-router/tests/routeContext.test.tsx | head -20

Repository: TanStack/router

Length of output: 137


🏁 Script executed:

# Let's understand how useRouteContext behaves - check if it's reactive and can change during component lifetime
rg "useRouteContext" packages/solid-router/tests/routeContext.test.tsx -A 2 | head -30

Repository: TanStack/router

Length of output: 981


🏁 Script executed:

# Check if there are other similar patterns in the test file to see how other tests track state changes
rg "createSignal" packages/solid-router/tests/routeContext.test.tsx -B 3 -A 3

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Look for documentation or examples of how context reactivity works in the codebase
fd -t f "*.tsx" packages/solid-router/tests | xargs grep -l "useRouteContext" | head -3

Repository: TanStack/router

Length of output: 344


🏁 Script executed:

# Let's check how useRouteContext is implemented to understand if it can return undefined
rg "useRouteContext" packages/solid-router/src -A 5 | head -40

Repository: TanStack/router

Length of output: 2582


🏁 Script executed:

# Look for context implementation and whether it becomes undefined during reloads
fd -type f -name "*.ts" -o -name "*.tsx" packages/solid-router/src | head -10

Repository: TanStack/router

Length of output: 230


🏁 Script executed:

# Check the beforeLoad and context propagation logic
rg "beforeLoad" packages/solid-router/src -B 2 -A 2 | head -50

Repository: TanStack/router

Length of output: 857


🏁 Script executed:

# Read the useRouteContext implementation to understand if it can return undefined
cat -n packages/solid-router/src/useRouteContext.ts

Repository: TanStack/router

Length of output: 1248


🏁 Script executed:

# Check if the context can change during component lifetime by looking at route state management
rg "context.*undefined|undefined.*context" packages/solid-router/src -B 2 -A 2 | head -30

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Look at the test more carefully - does the context actually change, or is it always available?
sed -n '2454,2528p' packages/solid-router/tests/routeContext.test.tsx

Repository: TanStack/router

Length of output: 2328


🏁 Script executed:

# Let's understand how context is managed during route transitions
# Check the useMatch implementation to see if it can become undefined
rg "useMatch" packages/solid-router/src/useMatch.ts -A 10 | head -50

Repository: TanStack/router

Length of output: 137


🏁 Script executed:

# Look at how routes handle context during loaders and transitions
rg "context.*undefined|becoming undefined" packages/solid-router -B 3 -A 3

Repository: TanStack/router

Length of output: 2926


🏁 Script executed:

# Check if there's documentation or comments about context stability during reloads
rg "reload.*context|context.*reload" packages/solid-router/tests -B 2 -A 2 | head -40

Repository: TanStack/router

Length of output: 1902


🏁 Script executed:

# Let's check the useMatch implementation in solid-router
find packages/solid-router/src -name "*.ts" -o -name "*.tsx" | xargs grep -l "useMatch"

Repository: TanStack/router

Length of output: 460


🏁 Script executed:

# Search for how match/context is obtained - look at the core import
rg "from '`@tanstack/router-core`'" packages/solid-router/src/useRouteContext.ts -A 5

Repository: TanStack/router

Length of output: 213


🏁 Script executed:

# Let's see what useMatch does by finding its implementation
rg "export.*useMatch" packages/solid-router/src -B 2 -A 10

Repository: TanStack/router

Length of output: 3521


🏁 Script executed:

# Check if context can become undefined by looking at route state during navigation
rg "staleTime.*0|background.*load|reload.*in.*flight" packages/solid-router -B 3 -A 3 | head -50

Repository: TanStack/router

Length of output: 3814


🏁 Script executed:

# Let's read the useMatch.tsx implementation to understand how context updates work
cat -n packages/solid-router/src/useMatch.tsx | head -80

Repository: TanStack/router

Length of output: 2737


🏁 Script executed:

# Check if createEffect is used in other component context tracking scenarios
rg "createEffect.*context|context.*createEffect" packages/solid-router/tests -B 3 -A 3

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Verify the test behavior - check if context is supposed to become undefined
rg "number\(\) === undefined|context.*undefined" packages/solid-router/tests/routeContext.test.tsx -B 3 -A 3

Repository: TanStack/router

Length of output: 528


🏁 Script executed:

# Let's check more of the useMatch implementation to confirm how it's reactive
cat -n packages/solid-router/src/useMatch.tsx | sed -n '77,120p'

Repository: TanStack/router

Length of output: 1526


🏁 Script executed:

# Verify Solid.js component lifecycle - does component function body re-run on reactivity changes?
web_search

Repository: TanStack/router

Length of output: 110


🌐 Web query:

Solid.js component function body execution once reactive dependencies

💡 Result:

In Solid.js, a component function is executed only once, at the time of its initial creation [1][2][3]. Because the component function is not re-run when its state or props change, the code inside the function body serves as a setup phase for initializing the reactive system [1][4]. To ensure updates occur in response to reactive dependencies, you must place those dependencies within reactive scopes, such as JSX expressions, createEffect, or createMemo [4]. When a signal or reactive value is accessed inside these scopes, Solid.js automatically tracks the dependency and updates only the affected part of the DOM, rather than re-running the component function [1][5][3]. Reading a signal directly in the component body will only capture its initial value and will not cause subsequent updates when that signal changes [4]. For example, to derive a value reactively: - Do not compute it directly in the component body (e.g., const val = signal * 2), as this will not update [5][4]. - Use a function wrapper or createMemo (e.g., const val = createMemo( => signal * 2)) to ensure the calculation is re-evaluated only when the dependency changes [4]. This "run-once" model is fundamental to Solid.js, allowing it to bypass the VDOM and re-rendering overhead typical of other frameworks [1][3]. The component function itself is wrapped in an untracked execution context during setup to prevent it from becoming a dependency of its own reactive contents [2].

Citations:


🏁 Script executed:

# Let's understand the test flow better - what does the test actually expect?
# Look for similar test patterns or comments
rg "undefined.*context|context.*undefined" packages/solid-router/tests/routeContext.test.tsx -B 5 -A 2 | head -60

Repository: TanStack/router

Length of output: 1000


🏁 Script executed:

# Check if there are any issues or PRs related to this test
git log --oneline packages/solid-router/tests/routeContext.test.tsx | head -10

Repository: TanStack/router

Length of output: 88


🏁 Script executed:

# Let's check if this test is currently passing or if it's a known issue
git log -p --all -S "sawUndefinedContext" packages/solid-router/tests/routeContext.test.tsx | head -100

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Check if context could actually become undefined during the test scenario
rg "beforeLoad.*undefined|context.*undefined.*beforeLoad" packages/solid-router -B 3 -A 3

Repository: TanStack/router

Length of output: 1523


🏁 Script executed:

# Let's get more git history to understand when this test was added and why
git log --oneline --all -- packages/solid-router/tests/routeContext.test.tsx | head -20

Repository: TanStack/router

Length of output: 232


🏁 Script executed:

# Check the recent commit that added this test
git show 9698aeed7 --stat | head -30

Repository: TanStack/router

Length of output: 1532


🏁 Script executed:

# Let's see the actual diff of when this test was added
git show 9698aeed7 -- packages/solid-router/tests/routeContext.test.tsx | head -150

Repository: TanStack/router

Length of output: 4400


🏁 Script executed:

# Let's search for the specific issues this commit fixes
rg "7602|7210|7457" . --type txt --type md 2>/dev/null | head -20

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Check the broader test to understand the full context lifecycle
sed -n '2454,2528p' packages/solid-router/tests/routeContext.test.tsx | cat -n

Repository: TanStack/router

Length of output: 2853


🏁 Script executed:

# Look at how useRouteContext accessor behaves - can it become undefined mid-render?
rg "undefined" packages/solid-router/src/useMatch.tsx -B 2 -A 2 | head -50

Repository: TanStack/router

Length of output: 1226


🏁 Script executed:

# Let's verify the component lifecycle more carefully - does the component stay mounted during loader reload?
# Check if re-entering the route causes a remount
rg "staleTime.*0" packages/solid-router/tests/routeContext.test.tsx -B 5 -A 15 | head -60

Repository: TanStack/router

Length of output: 674


🏁 Script executed:

# Search for other tests that track post-mount state changes
rg "createEffect.*number\|number.*createEffect" packages/solid-router/tests -B 3 -A 3

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Verify - in the test scenario, does the component get unmounted/remounted, or stay mounted during reload?
# Check if there's a specific behavior for background reloads
rg "background.*reload|reload.*background|in.*flight" packages/solid-router -B 3 -A 3 | head -40

Repository: TanStack/router

Length of output: 1778


🏁 Script executed:

# Let's check if createSignal is already imported in the test file
head -20 packages/solid-router/tests/routeContext.test.tsx | grep -E "import|createSignal"

Repository: TanStack/router

Length of output: 370


🏁 Script executed:

# Verify the exact issue - when component rerenders due to reactive update, does the body re-run?
# Let's search for similar reactive state tracking patterns in the solid-router tests
rg "createSignal.*=>" packages/solid-router/tests -B 2 -A 5 | head -60

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Understand the actual behavior: does the component body execute multiple times or just once?
web_search

Repository: TanStack/router

Length of output: 110


🌐 Web query:

Solid.js component function body executes once vs JSX expressions reactive

💡 Result:

In Solid.js, the component function body executes exactly once, serving as a setup function that initializes the reactive graph [1][2][3]. Unlike frameworks that re-render entire components when state changes, Solid components do not re-run after their initial mount [1][4][5]. The reactive behavior is achieved through the following mechanisms: 1. Setup Phase: During the component's single execution, Solid initializes signals, stores, and effects [1][6]. The JSX is compiled by the framework into fine-grained DOM operations [7][2][4]. 2. Tracking Scope: When JSX expressions (e.g., {count}) are encountered, they create reactive computations (often internally treated as effects) [6][8][9]. These expressions are subscribed to the signals they access [8][9]. 3. Fine-Grained Updates: When a signal's value changes, Solid does not re-execute the component [1][2]. Instead, it triggers only the specific reactive computations (such as individual text nodes or attribute updates in the JSX) that depend on that signal [2][3][9]. Because the component function only runs once, reading a signal directly in the component body (outside of a tracking scope like a JSX expression, createMemo, or createEffect) will only capture the signal's initial value [4][5]. Subsequent updates to that signal will not be reflected because the code that accessed it is never re-run [4][10]. To maintain reactivity, signal access must occur within a tracking scope, most commonly by placing the signal call directly inside the JSX return or within reactive primitives [4][5].

Citations:


🏁 Script executed:

# Let's check the actual component behavior - does number() ever become undefined during the test?
# If it does, the JSX would show "undefined" in the rendered output
rg "number = undefined" packages/solid-router/tests/routeContext.test.tsx

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Check if there are any other tests or issues that show context becoming undefined during reload
rg "undefined.*context|context.*undefined" packages/solid-router/tests/routeContext.test.tsx | head -20

Repository: TanStack/router

Length of output: 462


🏁 Script executed:

# Look at the commit more carefully - what specific issues was "fix: match loading" addressing?
git log --all --grep="7602\|7210\|7457" --oneline

Repository: TanStack/router

Length of output: 88


🏁 Script executed:

# Check if there's any code that could cause context to become undefined mid-component-lifecycle
rg "match.*undefined|selectedMatch.*undefined" packages/solid-router/src/useMatch.tsx -B 2 -A 2

Repository: TanStack/router

Length of output: 325


Use reactive state tracking to properly detect context changes during component lifetime.

The boolean assignment sawUndefinedContext ||= number() === undefined only executes during component creation because Solid.js component functions run once as a setup phase. If the context becomes undefined after the component mounts (e.g., during the background loader reload), the flag and rendered output will not update. Convert to a reactive pattern using a signal and createEffect to ensure any context loss is properly detected and reflected in the rendered output.

-import { createEffect, onMount } from 'solid-js'
+import { createEffect, createSignal, onMount } from 'solid-js'
-    let sawUndefinedContext = false
+    const [sawUndefinedContext, setSawUndefinedContext] = createSignal(false)
       component: () => {
         const context = reloadInFlightIndexRoute.useRouteContext()
         const number = () => context().number
-        sawUndefinedContext ||= number() === undefined
+        createEffect(() => {
+          if (number() === undefined) {
+            setSawUndefinedContext(true)
+          }
+        })
 
         return (
           <div>
             number = {String(number())}, saw undefined ={' '}
-            {String(sawUndefinedContext)}
+            {String(sawUndefinedContext())}
           </div>
         )
       },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/solid-router/tests/routeContext.test.tsx` around lines 2479 - 2487,
The `sawUndefinedContext` variable uses a one-time assignment during component
setup, so it will not detect if the context becomes undefined after the
component mounts. Replace the simple boolean assignment with a reactive pattern:
create a signal to store the `sawUndefinedContext` state and use `createEffect`
to reactively update it whenever `number()` changes, ensuring that any context
loss during the component's lifetime (such as during a background loader reload)
is properly detected and reflected in the rendered output.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/router-core/src/load-matches.ts (1)

599-610: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the resumed beforeLoad wait against evicted or replaced matches.

clearMatchPromises can resolve pendingBeforeLoad while cache cleanup removes the cached match. This continuation then dereferences getMatch(matchId)!, or can resume against a newer match with the same id. Capture the _nonReactive bucket and bail if the match disappeared or changed.

Suggested guard
       if (pendingBeforeLoad) {
+        const pendingBeforeLoadBucket = existingMatch._nonReactive
         return pendingBeforeLoad.then(() => {
-          const match = inner.router.getMatch(matchId)!
+          const match = inner.router.getMatch(matchId)
+          if (!match || match._nonReactive !== pendingBeforeLoadBucket) {
+            return
+          }
           if (match.preload && match.status === 'notFound') {
             handleRedirectOrNotFound(inner, match, match.error)
           }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/src/load-matches.ts` around lines 599 - 610, The
continuation after pendingBeforeLoad resolves can dereference a match that was
evicted or replaced during cache cleanup. Before entering the
pendingBeforeLoad.then() block in the if statement checking pendingBeforeLoad,
capture a reference to the current match's _nonReactive bucket or state. Then,
after pendingBeforeLoad resolves, validate that the match retrieved via
getMatch(matchId) is still the same match instance (not a newly created
replacement) by comparing against the captured reference. If the match has
disappeared or been replaced with a different instance, bail out early before
calling handleRedirectOrNotFound, getLoaderMatch, or executeBeforeLoad to
prevent operating on stale or incorrect match data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/router-core/src/router.ts`:
- Around line 2882-2886: The current logic in the `beforeLoad` promise check
skips waiting if `_nonReactive.beforeLoadPromise` is undefined, but this causes
a race condition where active pending matches whose beforeLoad phase has not yet
started can be bypassed. For active matches, we need to ensure their beforeLoad
phase has completed before proceeding, even if the promise hasn't been created
yet. Add a mechanism to detect when a match is active and has a pending
beforeLoad phase that hasn't begun, and ensure we wait for it to start and
complete before the code continues with `settledMatch` substitution. This could
involve adding an explicit per-match beforeLoad phase gate when pending matches
are created, or adding additional checks to identify and wait for active matches
with not-yet-started beforeLoad phases.

---

Outside diff comments:
In `@packages/router-core/src/load-matches.ts`:
- Around line 599-610: The continuation after pendingBeforeLoad resolves can
dereference a match that was evicted or replaced during cache cleanup. Before
entering the pendingBeforeLoad.then() block in the if statement checking
pendingBeforeLoad, capture a reference to the current match's _nonReactive
bucket or state. Then, after pendingBeforeLoad resolves, validate that the match
retrieved via getMatch(matchId) is still the same match instance (not a newly
created replacement) by comparing against the captured reference. If the match
has disappeared or been replaced with a different instance, bail out early
before calling handleRedirectOrNotFound, getLoaderMatch, or executeBeforeLoad to
prevent operating on stale or incorrect match data.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51059b2b-6851-4b78-993c-6741ca94f043

📥 Commits

Reviewing files that changed from the base of the PR and between 3cb972f and c55f431.

📒 Files selected for processing (5)
  • packages/react-router/tests/redirect.test.tsx
  • packages/router-core/src/load-matches.ts
  • packages/router-core/src/router.ts
  • packages/solid-router/tests/redirect.test.tsx
  • packages/vue-router/tests/redirect.test.tsx
💤 Files with no reviewable changes (3)
  • packages/solid-router/tests/redirect.test.tsx
  • packages/vue-router/tests/redirect.test.tsx
  • packages/react-router/tests/redirect.test.tsx

Comment thread packages/router-core/src/router.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/router-core/src/load-matches.ts (2)

607-616: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing null checks after async operations in queueExecution. Both sites use non-null assertions on getMatch results where the match may have been evicted during async operations, with null guards positioned after the problematic accesses.

  • packages/router-core/src/load-matches.ts#L607-L616: Guard match before accessing match.preload after awaiting pendingBeforeLoad.
  • packages/router-core/src/load-matches.ts#L620-L623: Guard match before accessing match.preload when loaderPromise is pending.
🛡️ Combined fix
       if (pendingBeforeLoad) {
         return pendingBeforeLoad.then(() => {
-          const match = inner.router.getMatch(matchId)!
-          if (match.preload && match.status === 'notFound') {
+          const match = inner.router.getMatch(matchId)
+          if (match?.preload && match.status === 'notFound') {
             handleRedirectOrNotFound(inner, match, match.error)
           }

           if (!getLoaderMatch(inner, matchId)) {
             return
           }
           return executeBeforeLoad(inner, matchId, index, route)
         })
       }

-      const match = inner.router.getMatch(matchId)!
-      if (match.preload && match.status === 'notFound') {
+      const match = inner.router.getMatch(matchId)
+      if (match?.preload && match.status === 'notFound') {
         handleRedirectOrNotFound(inner, match, match.error)
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/src/load-matches.ts` around lines 607 - 616, The
getMatch function in queueExecution may return null or have its result evicted
during async operations, but the code accesses properties like match.preload on
the non-null asserted result without verifying the match still exists. At
packages/router-core/src/load-matches.ts lines 607-616, add a null check on the
match variable returned from getMatch before accessing match.preload and
match.status. At packages/router-core/src/load-matches.ts lines 620-623, add a
similar null check on the match variable before accessing match.preload when
handling the loaderPromise pending case. In both locations, ensure the match
exists (is not null or undefined) immediately after calling getMatch, before
attempting to access any of its properties.

905-921: ⚠️ Potential issue | 🟡 Minor

Handle notFound errors in background stale-while-revalidate reload, or document intentional behavior.

The background IIFE catches errors but only handles isRedirect. When runLoader throws a notFound error (via handleNotFound), the error falls through the catch block without being re-thrown. This leaves the match in 'notFound' status while the error is silently swallowed, creating inconsistent state:

  • The match has already been patched to status: 'notFound' by handleNotFound inside runLoader
  • The error is not re-thrown or handled
  • settleLoadPromises still runs, completing as if the load succeeded

For proper SWR behavior, non-redirect errors should either be handled explicitly (similar to redirects) or re-thrown. Consider:

  • Adding isNotFound handling in the catch block (e.g., revert the status back to the previous state to preserve stale data)
  • Or re-throwing the error to let finalization handle it
  • Or adding a comment explaining why silently swallowing is intentional

Note: There is test coverage for background redirects but none for background notFound errors, suggesting this case may not be intentionally supported.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/src/load-matches.ts` around lines 905 - 921, The
background IIFE in the async wrapper catches errors but only handles redirects
via isRedirect check, allowing notFound errors from runLoader to be silently
swallowed while the match is already patched to 'notFound' status by
handleNotFound. Modify the catch block to explicitly handle notFound errors
(check using isNotFound or similar) by either reverting the match status back to
preserve stale data similar to how redirects are handled via router.navigate, or
re-throw the error to let proper finalization occur. If silently swallowing is
intentional, add a clear comment explaining this behavior. Consider whether
settleLoadPromises should still execute in the notFound case or only in
successful load scenarios.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/router-core/src/load-matches.ts`:
- Around line 607-616: The getMatch function in queueExecution may return null
or have its result evicted during async operations, but the code accesses
properties like match.preload on the non-null asserted result without verifying
the match still exists. At packages/router-core/src/load-matches.ts lines
607-616, add a null check on the match variable returned from getMatch before
accessing match.preload and match.status. At
packages/router-core/src/load-matches.ts lines 620-623, add a similar null check
on the match variable before accessing match.preload when handling the
loaderPromise pending case. In both locations, ensure the match exists (is not
null or undefined) immediately after calling getMatch, before attempting to
access any of its properties.
- Around line 905-921: The background IIFE in the async wrapper catches errors
but only handles redirects via isRedirect check, allowing notFound errors from
runLoader to be silently swallowed while the match is already patched to
'notFound' status by handleNotFound. Modify the catch block to explicitly handle
notFound errors (check using isNotFound or similar) by either reverting the
match status back to preserve stale data similar to how redirects are handled
via router.navigate, or re-throw the error to let proper finalization occur. If
silently swallowing is intentional, add a clear comment explaining this
behavior. Consider whether settleLoadPromises should still execute in the
notFound case or only in successful load scenarios.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f0024442-7f0e-42dc-8215-e0c9b9fe6050

📥 Commits

Reviewing files that changed from the base of the PR and between 07cde45 and 0bf8243.

📒 Files selected for processing (1)
  • packages/router-core/src/load-matches.ts

nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

@nx-cloud nx-cloud Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.

Nx Cloud is proposing a fix for your failed CI:

We fixed two code-change failures introduced by the PR's match loading refactor. The unnecessary non-null assertion (!) on inner.matches[index] in load-matches.ts was removed to satisfy the @typescript-eslint/no-unnecessary-type-assertion ESLint rule, since standard TypeScript array indexing already returns AnyRouteMatch without needing the assertion. We also updated the redirection in preload store-update count expectation in the Vue router test from 5 to 3, reflecting the PR's more efficient match loading which correctly produces fewer reactive updates for this navigation scenario.

Tip

We verified this fix by re-running @tanstack/router-core:test:eslint, @tanstack/vue-router:test:unit.

diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts
index 4d2d22b6..9cc9778d 100644
--- a/packages/router-core/src/load-matches.ts
+++ b/packages/router-core/src/load-matches.ts
@@ -86,7 +86,7 @@ const joinActivePreloadMatch = async (
       match._nonReactive.loaderPromise || match._nonReactive.loadPromise
     if (loaderPromise?.status === 'pending') {
       await loaderPromise
-      match = inner.router.getMatch(matchId) ?? inner.matches[index]!
+      match = inner.router.getMatch(matchId) ?? inner.matches[index]
     }
     inner.matches[index] = match
     error = match._nonReactive.error || match.error
diff --git a/packages/vue-router/tests/store-updates-during-navigation.test.tsx b/packages/vue-router/tests/store-updates-during-navigation.test.tsx
index 3a51d638..6ce87e8a 100644
--- a/packages/vue-router/tests/store-updates-during-navigation.test.tsx
+++ b/packages/vue-router/tests/store-updates-during-navigation.test.tsx
@@ -157,7 +157,7 @@ describe("Store doesn't update *too many* times during navigation", () => {
     // that needs to be done during a navigation.
     // Any change that increases this number should be investigated.
     // Note: Vue has different update counts than React/Solid due to different reactivity
-    expect(updates).toBe(5)
+    expect(updates).toBe(3)
     expect(
       router.stores.cachedMatches
         .get()

Apply fix via Nx Cloud  Reject fix via Nx Cloud


Or Apply changes locally with:

npx nx-cloud apply-locally B2ee-K2Av

Apply fix locally with your editor ↗   View interactive diff ↗



🎓 Learn more about Self-Healing CI on nx.dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment