Skip to content

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

Description

@thomastvedt

Which project does this relate to?

Router

Describe the bug

Since the granular stores refactor there is no public reactive way to read pendingMatches. It used to be part of RouterState, so you could do useRouterState({ select: (s) => s.pendingMatches }) (older issues like #925 quote this.state.pendingMatches directly). Now RouterState no longer carries matches/pendingMatches, and useMatches only selects over the committed matches. The data still exists in router.stores.pendingMatches, but no hook exposes it, so this looks like API surface that fell off in the refactor rather than a deliberate removal.

Why I care: I'm migrating a fairly big app from react-router, and our tab strips light up the clicked tab the moment a navigation starts instead of when the target's chunk and loader have resolved. Everything needed for that is already in the router, since the pending matches are built synchronously at navigation start and carry the target route's staticData (each route declares which tab it lights up there). I just can't get at them reactively.

The workaround I ended up with works but is not pretty. It reaches into router.stores and leans on event ordering (the pending stores happen to be populated before onBeforeLoad is emitted, and committed before onResolved):

function useActiveTab() {
  const router = useRouter()

  const subscribe = useCallback(
    (onChange: () => void) => {
      const subs = [
        router.subscribe('onBeforeLoad', onChange),
        router.subscribe('onResolved', onChange),
      ]
      return () => subs.forEach((unsub) => unsub())
    },
    [router],
  )

  return useSyncExternalStore(subscribe, () => {
    const pending = router.stores.pendingMatches.get()
    const matches = pending.length > 0 ? pending : router.stores.matches.get()
    return matches.findLast((m) => m.staticData.tab)?.staticData.tab
  })
}

I'd love either a usePendingMatches hook mirroring useMatches (select + structural sharing), or pendingMatches back on the state that useRouterState selects over. Happy to take a stab at a PR if you can point me at the preferred shape.

Complete minimal reproducer

Not really applicable (missing API rather than wrong behavior), but here is the gap in one snippet:

// worked before the stores refactor:
const pending = useRouterState({ select: (s) => s.pendingMatches })
// today: Property 'pendingMatches' does not exist on type 'RouterState<...>'

Steps to Reproduce the Bug

  1. useRouterState({ select: (s) => s.pendingMatches }) no longer type-checks (and the value is not on the state object)
  2. useMatches only covers committed matches
  3. There is no other exported hook that reads router.stores.pendingMatches

Expected behavior

Some supported reactive way to read the pending matches, like usePendingMatches({ select }) or pendingMatches on RouterState again.

Platform

  • @tanstack/react-router 1.170.18 / @tanstack/router-core 1.171.15
  • React 19, Vite

Additional context

Filing this as a bug rather than a discussion since the capability existed before the stores refactor and disappeared without a replacement. If you'd rather treat it as a feature request I can move it to discussions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions