Skip to content

WIP remove pending from store (2nd attempt)#6670

Draft
Sheraff wants to merge 4 commits intomainfrom
refactor-router-core-remove-pending-from-store-2
Draft

WIP remove pending from store (2nd attempt)#6670
Sheraff wants to merge 4 commits intomainfrom
refactor-router-core-remove-pending-from-store-2

Conversation

@Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Feb 15, 2026

Summary by CodeRabbit

  • Breaking Changes

    • Removed the public pendingMatches property; use router.state.status === 'pending' and route-matching against the latest location to detect pending matches.
  • Documentation

    • Updated API docs with migration notes on inferring pending state and retrieving pending matches.
  • Tests

    • Added pending-transition tests and updated existing test expectations for navigation/store-update behavior.
  • Devtools/UX

    • Devtools and tooling now surface pending matches based on status + latest-location matching.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 15, 2026

📝 Walkthrough

Walkthrough

This PR removes the public pendingMatches from RouterState and moves pending-match tracking to a private pendingMatchesInternal. Consumers are expected to infer pending state via router.state.status === 'pending' and router.matchRoutes(router.latestLocation). Docs, core logic, plugins, devtools, and framework hooks/tests updated accordingly.

Changes

Cohort / File(s) Summary
Documentation
docs/router/api/router/RouterStateType.md, docs/router/api/router/useChildMatchesHook.md, docs/router/api/router/useParentMatchesHook.md
Removed public pendingMatches from docs; added migration notes directing users to use router.state.status + router.matchRoutes(...).
Router Core
packages/router-core/src/router.ts
Moved pendingMatches out of public RouterState into a private pendingMatchesInternal; updated lifecycle, matching, cancellation, and commit paths to use the internal field and removed public exposure and initial state entry.
Router Core Tests
packages/router-core/tests/load.test.ts
Replaced assertions reading router.state.pendingMatches with status-based assertions (router.state.status === 'pending') and router.getMatch(...) lookups.
DevTools
packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx
Compute pendingMatches from router().matchRoutes(router().latestLocation) when status === 'pending'; use the derived list for displayedMatches and active match logic.
HMR Plugin
packages/router-plugin/src/core/route-hmr-statement.ts, packages/router-plugin/tests/add-hmr/snapshots/*
Replace router.state.pendingMatches checks with hasPendingRouteMatch computed from router.state.status === 'pending' and router.matchRoutes(router.latestLocation).some(...); snapshot updates mirror this logic.
Solid Router
packages/solid-router/src/useMatch.tsx, packages/solid-router/tests/useMatch.test.tsx, packages/solid-router/tests/store-updates-during-navigation.test.tsx
Replace pendingMatches lookup with hasPendingMatch derived via router.matchRoutes(router.latestLocation); add pending-transition tests and introduce useRouterState usage in tests; update store-update assertions.
Vue Router
packages/vue-router/src/useMatch.tsx, packages/vue-router/tests/useMatch.test.tsx, packages/vue-router/tests/store-updates-during-navigation.test.tsx
Same pattern as Solid: use router.matchRoutes(router.latestLocation) + status === 'pending' to detect pending matches; add pending-transition tests and adjust store-update assertions.
React/Solid/Vue store-update tests
packages/react-router/tests/store-updates-during-navigation.test.tsx, packages/solid-router/tests/store-updates-during-navigation.test.tsx, packages/vue-router/tests/store-updates-during-navigation.test.tsx
Adjusted expected update counts/ranges to reflect changed update behavior (tightened or relaxed expectations depending on framework).

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client
participant RouterCore
participant Matcher
participant Loader
participant DevTools
Client->>RouterCore: navigate(to)
RouterCore->>Matcher: matchRoutes(latestLocation)
Matcher-->>RouterCore: matched routes
RouterCore->>RouterCore: set pendingMatchesInternal; status='pending'
RouterCore->>Loader: run loaders for pending matches
Loader-->>RouterCore: resolve/reject
RouterCore->>RouterCore: commit changes; clear pendingMatchesInternal; status='idle'/'loading'
DevTools->>RouterCore: matchRoutes(latestLocation) (when status==='pending')
DevTools-->>Client: display pending state/ matches

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • schiller-manuel
  • nlynzaad

Poem

🐰
Hopping through routes with a twitchy nose,
Hidden matches where nobody knows.
Status says "pending", location will show,
I sniff with matchRoutes and onward I go.
A little hop, a thunk, the router grows. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% 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 title uses 'WIP' and '2nd attempt' which are vague process indicators rather than clearly describing the main technical change of removing pendingMatches from the public RouterState API. Revise the title to be more descriptive, such as 'Remove pendingMatches from public RouterState API' or 'Move pendingMatches to internal state', removing WIP indicators for clarity.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-router-core-remove-pending-from-store-2

No actionable comments were generated in the recent review. 🎉


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

@nx-cloud
Copy link

nx-cloud bot commented Feb 15, 2026

View your CI Pipeline Execution ↗ for commit 565a264

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

☁️ Nx Cloud last updated this comment at 2026-02-15 16:30:04 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 15, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@6670

@tanstack/eslint-plugin-router

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

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@6670

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@6670

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@6670

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@6670

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-router-ssr-query

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

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@6670

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@6670

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@6670

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@6670

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-fn-stubs@6670

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@6670

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@6670

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@6670

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@6670

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@6670

@tanstack/vue-router

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

@tanstack/vue-router-devtools

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

@tanstack/vue-router-ssr-query

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

@tanstack/vue-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start@6670

@tanstack/vue-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-client@6670

@tanstack/vue-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-server@6670

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@6670

commit: 565a264

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (2)
packages/vue-router/src/useMatch.tsx (1)

80-94: ⚠️ Potential issue | 🟠 Major

Import RouterState and properly type the state parameter to maintain strict type safety.

The select callback in useRouterState expects (state: RouterState<TRouter['routeTree']>) => TSelected, but the current code passes (state: any), breaking the type contract. Similarly, (d: any) in the pending match check should be typed via the inferred return type from matchRoutes.

Suggested refactor
import type {
  AnyRouter,
  MakeRouteMatch,
  MakeRouteMatchUnion,
  RegisteredRouter,
+ RouterState,
  StrictOrFrom,
  ThrowConstraint,
  ThrowOrOptional,
} from '@tanstack/router-core'
  const matchSelection = useRouterState({
-   select: (state: any) => {
+   select: (state: RouterState<TRouter['routeTree']>) => {
      const match = state.matches.find((d: any) =>
        opts.from ? opts.from === d.routeId : d.id === nearestMatchId.value,
      )

      if (match === undefined) {
-       const hasPendingMatch =
+       const pendingMatches = router.matchRoutes(router.latestLocation)
+       const hasPendingMatch =
          state.status === 'pending' &&
-         router
-           .matchRoutes(router.latestLocation)
-           .some((d: any) =>
+         pendingMatches.some((d) =>
            opts.from
              ? opts.from === d.routeId
              : d.id === nearestMatchId.value,
          )

As per coding guidelines: **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety for all code.

packages/solid-router/src/useMatch.tsx (1)

83-95: ⚠️ Potential issue | 🟠 Major

Add RouterState type to the selector function to maintain strict type safety.

The select function parameter and match element callbacks use any instead of the properly typed RouterState. Import RouterState from @tanstack/router-core and type the selector as select: (state: RouterState<TRouter['routeTree']>) => { ... } to align with the TypeScript strict mode guideline and existing patterns in the codebase (see Matches.tsx).

🤖 Fix all issues with AI agents
In `@docs/router/api/router/RouterStateType.md`:
- Line 6: Update the documentation sentence for the RouterState type to include
the missing article for clarity: change the phrase so it reads "The
`RouterState` type represents the shape of the internal state of the router."
(refer to the `RouterState` type description) and similarly ensure the following
sentence reads "The Router's internal state..." where appropriate for consistent
readability.

In `@packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx`:
- Around line 282-289: The createMemo callbacks for pendingMatches and
displayedMatches are using implicit/any types which breaks strict TS guarantees;
update the createMemo calls for pendingMatches and displayedMatches to use
explicit generic type parameters (e.g., createMemo<AnyRouteMatch[]>) so the
returned arrays are strongly typed, remove any explicit any annotations in the
callbacks and downstream map/find usages, and ensure the expressions using
routerState(), router().matchRoutes(router().latestLocation) and
routerState().matches preserve the new typed signature.
🧹 Nitpick comments (3)
packages/solid-router/tests/useMatch.test.tsx (1)

184-243: Good test coverage for the complementary case.

The test correctly validates that useMatch still throws when the target route (/posts) is not in the pending matches, even when the router is in a pending state for another route (/other).

Consider adding a final assertion after navigation completes to verify the router reaches the expected state:

🔧 Optional: Add assertion for final state
       resolveOtherLoader()
       await navigation
+      expect(await screen.findByText('OtherTitle')).toBeInTheDocument()
     })
packages/vue-router/tests/useMatch.test.tsx (1)

185-244: Add a final assertion to confirm navigation completes.
This prevents the test from passing if the transition aborts after the invariant error.

✅ Suggested assertion
      resolveOtherLoader()
      await navigation
+     expect(await screen.findByText('OtherTitle')).toBeInTheDocument()
packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx (1)

143-148: Avoid recomputing matchRoutes per RouteComp while pending.

This memo calls router().matchRoutes(router().latestLocation) for every route row when pending, which can multiply matcher cost on large trees. Consider computing pending matches once in the panel and passing an accessor down to RouteComp.

---

The `RouterState` type represents shape of the internal state of the router. The Router's internal state is useful, if you need to access certain internals of the router, such as any pending matches, is the router in its loading state, etc.
The `RouterState` type represents shape of the internal state of the router. The Router's internal state is useful if you need to access certain internals of the router, such as whether it is currently loading.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Grammar: add “the” for readability.

✏️ Suggested edit
-The `RouterState` type represents shape of the internal state of the router.
+The `RouterState` type represents the shape of the internal state of the router.
📝 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
The `RouterState` type represents shape of the internal state of the router. The Router's internal state is useful if you need to access certain internals of the router, such as whether it is currently loading.
The `RouterState` type represents the shape of the internal state of the router.
🤖 Prompt for AI Agents
In `@docs/router/api/router/RouterStateType.md` at line 6, Update the
documentation sentence for the RouterState type to include the missing article
for clarity: change the phrase so it reads "The `RouterState` type represents
the shape of the internal state of the router." (refer to the `RouterState` type
description) and similarly ensure the following sentence reads "The Router's
internal state..." where appropriate for consistent readability.

Comment on lines +282 to +289
const pendingMatches = createMemo(() =>
routerState().status === 'pending'
? router().matchRoutes(router().latestLocation)
: [],
)
const displayedMatches = createMemo(() =>
pendingMatches().length ? pendingMatches() : routerState().matches,
)
Copy link
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, locate the file
find . -type f -name "BaseTanStackRouterDevtoolsPanel.tsx" | head -5

Repository: TanStack/router

Length of output: 130


🏁 Script executed:

# Check file size to determine how to read it
wc -l packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx

Repository: TanStack/router

Length of output: 132


🏁 Script executed:

# Read the relevant sections mentioned in the review
sed -n '280,295p' packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx

Repository: TanStack/router

Length of output: 665


🏁 Script executed:

# Also check lines 532 and 687-694
sed -n '530,535p' packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx

Repository: TanStack/router

Length of output: 311


🏁 Script executed:

sed -n '685,700p' packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx

Repository: TanStack/router

Length of output: 743


🏁 Script executed:

# Search for AnyRouteMatch type definition
rg "AnyRouteMatch" --type ts --type tsx

Repository: TanStack/router

Length of output: 86


🏁 Script executed:

# Check the imports in the file to understand available types
head -50 packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx

Repository: TanStack/router

Length of output: 1457


Tighten types for pending/displayed matches to keep strict TS guarantees.

Explicit any in the createMemo callbacks and map/find operations undermines strict typing. Adding explicit type parameters <AnyRouteMatch[]> to both createMemo calls removes the need for any type annotations and keeps usages typed throughout.

♻️ Proposed fix
-    const pendingMatches = createMemo(() =>
+    const pendingMatches = createMemo<AnyRouteMatch[]>(() =>
       routerState().status === 'pending'
         ? router().matchRoutes(router().latestLocation)
         : [],
     )
-    const displayedMatches = createMemo(() =>
+    const displayedMatches = createMemo<AnyRouteMatch[]>(() =>
       pendingMatches().length ? pendingMatches() : routerState().matches,
     )
-                    {displayedMatches().map((match: any, _i: any) => {
+                    {displayedMatches().map((match) => {
-                    {pendingMatches().find(
-                      (d: any) => d.id === activeMatch()?.id,
-                    )
+                    {pendingMatches().find(
+                      (d) => d.id === activeMatch()?.id,
+                    )

This addresses the coding guideline: **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety for all code. Similar issues exist at lines 532 and 687–694.

🤖 Prompt for AI Agents
In `@packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx` around
lines 282 - 289, The createMemo callbacks for pendingMatches and
displayedMatches are using implicit/any types which breaks strict TS guarantees;
update the createMemo calls for pendingMatches and displayedMatches to use
explicit generic type parameters (e.g., createMemo<AnyRouteMatch[]>) so the
returned arrays are strongly typed, remove any explicit any annotations in the
callbacks and downstream map/find usages, and ensure the expressions using
routerState(), router().matchRoutes(router().latestLocation) and
routerState().matches preserve the new typed signature.

walkReplaceSegmentTree(newRoute, router.processedTree.segmentTree);
const filter = m => m.routeId === oldRoute.id;
if (router.state.matches.find(filter) || router.state.pendingMatches?.find(filter)) {
const hasPendingRouteMatch = router.state.status === "pending" && router.matchRoutes(router.latestLocation).some(filter);
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps, defer having to ever having to run this IF condition which if successful checks for pending matches and keep it to only do so when the left-hand of the OR fails?

Suggested change
const hasPendingRouteMatch = router.state.status === "pending" && router.matchRoutes(router.latestLocation).some(filter);
const checkHasPendingRouteMatch = () => router.state.status === "pending" && router.matchRoutes(router.latestLocation).some(filter);
if (router.state.matches(filter) || checkHasPendingRouteMatch()) {

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants