Skip to content

feat: add usePendingMatches hook#7863

Open
thomastvedt wants to merge 2 commits into
TanStack:mainfrom
thomastvedt:feat/use-pending-matches
Open

feat: add usePendingMatches hook#7863
thomastvedt wants to merge 2 commits into
TanStack:mainfrom
thomastvedt:feat/use-pending-matches

Conversation

@thomastvedt

@thomastvedt thomastvedt commented Jul 20, 2026

Copy link
Copy Markdown

Closes #7859

Since the granular stores refactor, pendingMatches is no longer part of RouterState, so there is no public reactive way to read the matches of an in-flight navigation — useRouterState({ select: (s) => s.pendingMatches }) no longer compiles. The data still exists on router.stores.pendingMatches (already a derived reactive store, cleared on commit), but nothing public subscribes to it.

This PR adds a usePendingMatches hook mirroring useMatches over router.stores.pendingMatches, in all three adapters:

  • react-router: same shape as useMatches, including select, structuralSharing and the SSR short-circuit
  • solid-router: createMemo + replaceEqualDeep, same as useMatches
  • vue-router: useStore over the pending store, same as useMatches

The hook returns an empty array when no navigation is in flight.

Use case (from the issue): when migrating from react-router, reactively highlighting the destination navigation item from the pending matches' staticData as soon as navigation starts, before code-split chunks and loaders resolve — without reaching into router.stores and hand-rolling useSyncExternalStore over router events.

Also included:

  • Docs page docs/router/api/router/usePendingMatchesHook.md, linked from the API index
  • Unit tests for all three adapters: pending matches expose the destination path while a loader is blocked, and clear once the navigation resolves

test:eslint, test:types and test:unit pass for the three packages. Developed with AI assistance (Claude Code).

Summary by CodeRabbit

  • New Features
    • Added usePendingMatches for React, Solid, and Vue.
    • Exposes destination route matches during in-progress navigation (pending matches), with a select option to derive custom values.
    • Pending matches reset once navigation completes.
  • Documentation
    • Updated Router API docs and added a new guide page with examples for usePendingMatches.
  • Tests
    • Added coverage verifying pending match behavior while navigation loaders are unresolved and after completion.

Expose the router's pending matches reactively in the react, solid and
vue adapters, mirroring useMatches over router.stores.pendingMatches.
pendingMatches was selectable via useRouterState before the granular
stores refactor; the store still exists but had no public reactive
accessor.

Closes TanStack#7859

Claude-Session: https://claude.ai/code/session_0194gnPcL2Vh2fiESjL4YdKZ
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 853240c4-7a3c-4ba2-9503-e8c6ab7a8f42

📥 Commits

Reviewing files that changed from the base of the PR and between d48b08a and 357e0ce.

📒 Files selected for processing (2)
  • docs/router/api/router/usePendingMatchesHook.md
  • packages/vue-router/tests/Matches.test.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/vue-router/tests/Matches.test.tsx
  • docs/router/api/router/usePendingMatchesHook.md

📝 Walkthrough

Walkthrough

Adds usePendingMatches to the React, Solid, and Vue routers, exposes it through package entry points, tests pending navigation behavior, and documents its options and return contract.

Changes

Pending matches API

Layer / File(s) Summary
Framework hook implementations
packages/react-router/src/Matches.tsx, packages/solid-router/src/Matches.tsx, packages/vue-router/src/Matches.tsx
Adds framework-specific reactive hooks that read router.stores.pendingMatches and support optional selection.
Public exports and navigation tests
packages/*-router/src/index.tsx, packages/*-router/tests/Matches.test.tsx
Re-exports usePendingMatches and verifies selected destination paths during unresolved and completed navigations.
Hook documentation
docs/router/api/router.md, docs/router/api/router/usePendingMatchesHook.md
Adds the API index entry and documents behavior, options, return values, and usage.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Router
  participant PendingMatchesHook
  participant DestinationLoader
  User->>Router: click destination link
  Router->>PendingMatchesHook: expose destination pending match
  PendingMatchesHook-->>User: selected destination pathname
  Router->>DestinationLoader: await route loader
  DestinationLoader-->>Router: resolve loader
  Router->>PendingMatchesHook: clear pending matches
Loading

Possibly related PRs

Suggested labels: package: react-router, package: vue-router

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding the usePendingMatches hook.
Linked Issues check ✅ Passed The PR adds a supported reactive pendingMatches API with selection support across adapters, matching issue #7859's requested replacement.
Out of Scope Changes check ✅ Passed The changes stay within the hook, docs, exports, and tests needed to support pending matches and do not introduce unrelated work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 5

🤖 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 `@docs/router/api/router/usePendingMatchesHook.md`:
- Around line 6-8: Update the usePendingMatches documentation to state that
destination RouteMatch objects are available during any in-flight navigation,
including navigations blocked by pending conditions, not only while loading.
Preserve the existing behavior description for resolved or idle navigations and
the useMatches reference.
- Around line 23-28: Update the `opts.structuralSharing` documentation in
`usePendingMatchesHook` so it is explicitly scoped to the React adapter rather
than presented as a universal option. Do not claim Solid or Vue support unless
their hook option types and implementations are updated to expose it.

In `@packages/solid-router/src/Matches.tsx`:
- Around line 233-234: Update the conditional return logic in Matches.tsx to
wrap the prev === undefined branch in curly braces, and replace the any cast on
replaceEqualDeep with UseMatchesResult to preserve strict typing.

In `@packages/vue-router/src/Matches.tsx`:
- Around line 311-314: Update the fallback branch in the Matches selection logic
to remove the any cast and use the appropriate strict TypeScript type for
matches, while preserving the existing opts.select behavior and return value.

In `@packages/vue-router/tests/Matches.test.tsx`:
- Around line 386-389: Remove the explicit Array<any> annotation from the select
callback in usePendingMatches and let TypeScript infer the matches parameter
type. Preserve the existing last-match pathname selection and empty-string
fallback.
🪄 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: ea2d10bb-f2e0-46b4-b7bd-50d732ddb506

📥 Commits

Reviewing files that changed from the base of the PR and between edf5575 and d48b08a.

📒 Files selected for processing (11)
  • docs/router/api/router.md
  • docs/router/api/router/usePendingMatchesHook.md
  • packages/react-router/src/Matches.tsx
  • packages/react-router/src/index.tsx
  • packages/react-router/tests/Matches.test.tsx
  • packages/solid-router/src/Matches.tsx
  • packages/solid-router/src/index.tsx
  • packages/solid-router/tests/Matches.test.tsx
  • packages/vue-router/src/Matches.tsx
  • packages/vue-router/src/index.tsx
  • packages/vue-router/tests/Matches.test.tsx

Comment on lines +6 to +8
The `usePendingMatches` hook returns the [`RouteMatch`](./RouteMatchType.md) objects for the location the router is currently navigating to. While a navigation is loading, these are the matches for the destination location. Once the navigation resolves (or when no navigation is in flight), the array is empty and the resolved matches are available via [`useMatches`](./useMatchesHook.md).

This is useful for optimistic UI during navigation, e.g. highlighting the navigation item of the destination route from its `staticData` before its chunks and loaders have finished.

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document blocked navigations explicitly.

The PR objective includes pending destination matches during blocked navigation, but this text only describes a navigation that is “loading.” Clarify that pending matches are available during any in-flight navigation, including blocked navigation.

🤖 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 `@docs/router/api/router/usePendingMatchesHook.md` around lines 6 - 8, Update
the usePendingMatches documentation to state that destination RouteMatch objects
are available during any in-flight navigation, including navigations blocked by
pending conditions, not only while loading. Preserve the existing behavior
description for resolved or idle navigations and the useMatches reference.

Comment thread docs/router/api/router/usePendingMatchesHook.md
Comment on lines +233 to +234
if (prev === undefined) return res
return replaceEqualDeep(prev, res) as any

@coderabbitai coderabbitai Bot Jul 20, 2026

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add curly braces and improve type safety.

As per coding guidelines, control statements like if must use curly braces. Additionally, replace the any cast with UseMatchesResult for strict type safety.

🐛 Proposed fix
-    if (prev === undefined) return res
-    return replaceEqualDeep(prev, res) as any
+    if (prev === undefined) {
+      return res
+    }
+    return replaceEqualDeep(prev, res) as UseMatchesResult<TRouter, TSelected>
📝 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
if (prev === undefined) return res
return replaceEqualDeep(prev, res) as any
if (prev === undefined) {
return res
}
return replaceEqualDeep(prev, res) as UseMatchesResult<TRouter, TSelected>
🤖 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/src/Matches.tsx` around lines 233 - 234, Update the
conditional return logic in Matches.tsx to wrap the prev === undefined branch in
curly braces, and replace the any cast on replaceEqualDeep with UseMatchesResult
to preserve strict typing.

Source: Coding guidelines

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The typed cast doesn't compile here: createMemo then infers the callback's return as TSelected | UseMatchesResult<...>, which conflicts with the prev: TSelected | undefined parameter (TS2345). The as any is what collapses that inference conflict — it's the same reason useMatches directly above is written this way, and this hook deliberately mirrors it. Happy to change both in a follow-up if maintainers prefer another shape.

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.

@thomastvedt, agreed — the typed-cast recommendation is incorrect here. Since usePendingMatches intentionally mirrors useMatches, retaining as any is appropriate to avoid the createMemo generic inference conflict you described.

The only remaining part of the finding is the project’s curly-brace style requirement; no type-cast change is needed in this PR.


✏️ Learnings added
Learnt from: thomastvedt
Repo: TanStack/router PR: 7863
File: packages/solid-router/src/Matches.tsx:233-234
Timestamp: 2026-07-20T20:01:01.454Z
Learning: In `packages/solid-router/src/Matches.tsx`, both `useMatches` and `usePendingMatches` use `as any` on the `replaceEqualDeep` result because casting to `UseMatchesResult<TRouter, TSelected>` causes a `Solid.createMemo` generic inference conflict (TS2345): the callback return is inferred as `TSelected | UseMatchesResult<TRouter, TSelected>`, incompatible with its `prev: TSelected | undefined` parameter.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread packages/vue-router/src/Matches.tsx
Comment thread packages/vue-router/tests/Matches.test.tsx
…ructuralSharing

Also rely on type inference for the select callback in the vue test.

Claude-Session: https://claude.ai/code/session_0194gnPcL2Vh2fiESjL4YdKZ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No reactive way to read pendingMatches since the stores refactor (regression, was on RouterState)

1 participant