feat: add usePendingMatches hook#7863
Conversation
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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds ChangesPending matches API
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
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (11)
docs/router/api/router.mddocs/router/api/router/usePendingMatchesHook.mdpackages/react-router/src/Matches.tsxpackages/react-router/src/index.tsxpackages/react-router/tests/Matches.test.tsxpackages/solid-router/src/Matches.tsxpackages/solid-router/src/index.tsxpackages/solid-router/tests/Matches.test.tsxpackages/vue-router/src/Matches.tsxpackages/vue-router/src/index.tsxpackages/vue-router/tests/Matches.test.tsx
| 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. |
There was a problem hiding this comment.
🎯 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.
| if (prev === undefined) return res | ||
| return replaceEqualDeep(prev, res) as any |
There was a problem hiding this comment.
📐 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.
| 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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
…ructuralSharing Also rely on type inference for the select callback in the vue test. Claude-Session: https://claude.ai/code/session_0194gnPcL2Vh2fiESjL4YdKZ
Closes #7859
Since the granular stores refactor,
pendingMatchesis no longer part ofRouterState, 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 onrouter.stores.pendingMatches(already a derived reactive store, cleared on commit), but nothing public subscribes to it.This PR adds a
usePendingMatcheshook mirroringuseMatchesoverrouter.stores.pendingMatches, in all three adapters:useMatches, includingselect,structuralSharingand the SSR short-circuitcreateMemo+replaceEqualDeep, same asuseMatchesuseStoreover the pending store, same asuseMatchesThe 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'
staticDataas soon as navigation starts, before code-split chunks and loaders resolve — without reaching intorouter.storesand hand-rollinguseSyncExternalStoreover router events.Also included:
docs/router/api/router/usePendingMatchesHook.md, linked from the API indextest:eslint,test:typesandtest:unitpass for the three packages. Developed with AI assistance (Claude Code).Summary by CodeRabbit
usePendingMatchesfor React, Solid, and Vue.selectoption to derive custom values.usePendingMatches.