From 8f6309b697cac0bac85134c510e3a6a454522d84 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Wed, 15 Jul 2026 03:17:00 -0700 Subject: [PATCH 1/2] fix: upgrade TanStack Store to latest --- packages/react-router/package.json | 2 +- packages/react-router/src/Match.tsx | 11 ++++-- packages/router-core/package.json | 2 +- packages/vue-router/package.json | 2 +- packages/vue-router/src/Match.tsx | 4 +-- packages/vue-router/src/Matches.tsx | 2 +- packages/vue-router/src/Scripts.tsx | 2 +- packages/vue-router/src/Transitioner.tsx | 3 +- packages/vue-router/src/headContentUtils.tsx | 2 +- packages/vue-router/src/link.tsx | 10 +++--- packages/vue-router/src/not-found.tsx | 2 +- packages/vue-router/src/useCanGoBack.ts | 2 +- packages/vue-router/src/useLocation.tsx | 2 +- packages/vue-router/src/useMatch.tsx | 4 +-- packages/vue-router/src/useRouterState.tsx | 2 +- packages/vue-router/src/useStore.ts | 37 +++++++++++++++++++ pnpm-lock.yaml | 38 ++++++++++---------- 17 files changed, 87 insertions(+), 40 deletions(-) create mode 100644 packages/vue-router/src/useStore.ts diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 9f4bbd6b90..e03847a15f 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -93,7 +93,7 @@ }, "dependencies": { "@tanstack/history": "workspace:*", - "@tanstack/react-store": "^0.9.3", + "@tanstack/react-store": "^0.11.0", "@tanstack/router-core": "workspace:*", "isbot": "^5.1.22" }, diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx index 5de5546aeb..0dab08b4f6 100644 --- a/packages/react-router/src/Match.tsx +++ b/packages/react-router/src/Match.tsx @@ -40,6 +40,13 @@ const outletMatchSelectionEqual = ( b: OutletMatchSelection, ) => a[0] === b[0] && a[1] === b[1] +const emptyMatchStore = { + get: () => undefined as AnyRouteMatch | undefined, + subscribe: (_listener: (value: AnyRouteMatch | undefined) => void) => ({ + unsubscribe() {}, + }), +} + export const Match = React.memo(function MatchImpl({ matchId, }: { @@ -550,8 +557,8 @@ export const Outlet = React.memo(function OutletImpl() { // Subscribe directly to the match store from the pool instead of // the two-level byId → matchStore pattern. const parentMatchStore = matchId - ? router.stores.matchStores.get(matchId) - : undefined + ? (router.stores.matchStores.get(matchId) ?? emptyMatchStore) + : emptyMatchStore // eslint-disable-next-line react-hooks/rules-of-hooks ;[routeId, parentGlobalNotFound] = useStore( diff --git a/packages/router-core/package.json b/packages/router-core/package.json index 8f8f23f639..872f832cb6 100644 --- a/packages/router-core/package.json +++ b/packages/router-core/package.json @@ -188,7 +188,7 @@ "seroval-plugins": "^1.5.4" }, "devDependencies": { - "@tanstack/store": "^0.9.3", + "@tanstack/store": "^0.11.0", "@types/node": "25.0.9", "esbuild": "^0.27.4", "vite": "*" diff --git a/packages/vue-router/package.json b/packages/vue-router/package.json index 96dc3fde40..a26da3f719 100644 --- a/packages/vue-router/package.json +++ b/packages/vue-router/package.json @@ -75,7 +75,7 @@ "dependencies": { "@tanstack/history": "workspace:*", "@tanstack/router-core": "workspace:*", - "@tanstack/vue-store": "^0.9.3", + "@tanstack/vue-store": "^0.11.0", "@vue/runtime-dom": "^3.5.25", "isbot": "^5.1.22" }, diff --git a/packages/vue-router/src/Match.tsx b/packages/vue-router/src/Match.tsx index 44e9aca0cd..a029c14194 100644 --- a/packages/vue-router/src/Match.tsx +++ b/packages/vue-router/src/Match.tsx @@ -8,7 +8,7 @@ import { rootRouteId, } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' -import { useStore } from '@tanstack/vue-store' +import { useStore } from './useStore' import { CatchBoundary, ErrorComponent } from './CatchBoundary' import { ClientOnly } from './ClientOnly' import { useRouter } from './useRouter' @@ -64,7 +64,7 @@ export const Match = Vue.defineComponent({ const isPendingMatchRef = useStore( router.stores.pendingRouteIds, (pendingRouteIds) => Boolean(pendingRouteIds[routeId]), - { equal: Object.is }, + Object.is, ) const loadedAt = useStore(router.stores.loadedAt, (value) => value) diff --git a/packages/vue-router/src/Matches.tsx b/packages/vue-router/src/Matches.tsx index da8220db29..273e57e998 100644 --- a/packages/vue-router/src/Matches.tsx +++ b/packages/vue-router/src/Matches.tsx @@ -1,6 +1,6 @@ import * as Vue from 'vue' import { isServer } from '@tanstack/router-core/isServer' -import { useStore } from '@tanstack/vue-store' +import { useStore } from './useStore' import { CatchBoundary } from './CatchBoundary' import { useRouter } from './useRouter' import { useTransitionerSetup } from './Transitioner' diff --git a/packages/vue-router/src/Scripts.tsx b/packages/vue-router/src/Scripts.tsx index 32c9eef8b5..221eec5a5c 100644 --- a/packages/vue-router/src/Scripts.tsx +++ b/packages/vue-router/src/Scripts.tsx @@ -1,7 +1,7 @@ import * as Vue from 'vue' -import { useStore } from '@tanstack/vue-store' import { isServer } from '@tanstack/router-core/isServer' import { Asset } from './Asset' +import { useStore } from './useStore' import { useRouter } from './useRouter' import type { RouterManagedTag } from '@tanstack/router-core' diff --git a/packages/vue-router/src/Transitioner.tsx b/packages/vue-router/src/Transitioner.tsx index b0133d965c..a0fa3f8b56 100644 --- a/packages/vue-router/src/Transitioner.tsx +++ b/packages/vue-router/src/Transitioner.tsx @@ -1,7 +1,8 @@ import * as Vue from 'vue' import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' -import { batch, useStore } from '@tanstack/vue-store' +import { batch } from '@tanstack/vue-store' +import { useStore } from './useStore' import { useRouter } from './useRouter' import { usePrevious } from './utils' diff --git a/packages/vue-router/src/headContentUtils.tsx b/packages/vue-router/src/headContentUtils.tsx index 1f1f8ff973..20a4945260 100644 --- a/packages/vue-router/src/headContentUtils.tsx +++ b/packages/vue-router/src/headContentUtils.tsx @@ -6,7 +6,7 @@ import { getScriptPreloadAttrs, resolveManifestCssLink, } from '@tanstack/router-core' -import { useStore } from '@tanstack/vue-store' +import { useStore } from './useStore' import { useRouter } from './useRouter' import type { AssetCrossOriginConfig, diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index 3a5a9875a8..0c5a8057f3 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -9,7 +9,7 @@ import { } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' -import { useStore } from '@tanstack/vue-store' +import { useStore } from './useStore' import { useRouter } from './useRouter' import { useIntersectionObserver } from './utils' @@ -219,9 +219,11 @@ export function useLinkProps< ) as unknown as LinkHTMLAttributes } - const currentLocation = useStore(router.stores.location, (l) => l, { - equal: (prev, next) => prev.href === next.href, - }) + const currentLocation = useStore( + router.stores.location, + (l) => l, + (prev, next) => prev.href === next.href, + ) const next = Vue.computed(() => { // Rebuild when inherited search/hash or the current route context changes. diff --git a/packages/vue-router/src/not-found.tsx b/packages/vue-router/src/not-found.tsx index f676c66e99..5eb26583f1 100644 --- a/packages/vue-router/src/not-found.tsx +++ b/packages/vue-router/src/not-found.tsx @@ -1,6 +1,6 @@ import * as Vue from 'vue' import { isNotFound } from '@tanstack/router-core' -import { useStore } from '@tanstack/vue-store' +import { useStore } from './useStore' import { CatchBoundary } from './CatchBoundary' import { useRouter } from './useRouter' import type { ErrorComponentProps, NotFoundError } from '@tanstack/router-core' diff --git a/packages/vue-router/src/useCanGoBack.ts b/packages/vue-router/src/useCanGoBack.ts index 2149584979..0b363f0923 100644 --- a/packages/vue-router/src/useCanGoBack.ts +++ b/packages/vue-router/src/useCanGoBack.ts @@ -1,4 +1,4 @@ -import { useStore } from '@tanstack/vue-store' +import { useStore } from './useStore' import { useRouter } from './useRouter' export function useCanGoBack() { diff --git a/packages/vue-router/src/useLocation.tsx b/packages/vue-router/src/useLocation.tsx index 2d9e4314a2..b21332fe5b 100644 --- a/packages/vue-router/src/useLocation.tsx +++ b/packages/vue-router/src/useLocation.tsx @@ -1,4 +1,4 @@ -import { useStore } from '@tanstack/vue-store' +import { useStore } from './useStore' import { useRouter } from './useRouter' import type { AnyRouter, diff --git a/packages/vue-router/src/useMatch.tsx b/packages/vue-router/src/useMatch.tsx index 29047e5d48..72e0b656e4 100644 --- a/packages/vue-router/src/useMatch.tsx +++ b/packages/vue-router/src/useMatch.tsx @@ -1,7 +1,7 @@ import * as Vue from 'vue' import { invariant } from '@tanstack/router-core' -import { useStore } from '@tanstack/vue-store' import { isServer } from '@tanstack/router-core/isServer' +import { useStore } from './useStore' import { injectDummyPendingMatch, injectPendingMatch, @@ -147,7 +147,7 @@ export function useMatch< const isTransitioning = useStore( router.stores.isTransitioning, (value) => value, - { equal: Object.is }, + Object.is, ) const result = Vue.computed(() => { diff --git a/packages/vue-router/src/useRouterState.tsx b/packages/vue-router/src/useRouterState.tsx index 13b6f85236..1749f15442 100644 --- a/packages/vue-router/src/useRouterState.tsx +++ b/packages/vue-router/src/useRouterState.tsx @@ -1,6 +1,6 @@ import * as Vue from 'vue' import { isServer } from '@tanstack/router-core/isServer' -import { useStore } from '@tanstack/vue-store' +import { useStore } from './useStore' import { useRouter } from './useRouter' import type { AnyRouter, diff --git a/packages/vue-router/src/useStore.ts b/packages/vue-router/src/useStore.ts new file mode 100644 index 0000000000..77d965211d --- /dev/null +++ b/packages/vue-router/src/useStore.ts @@ -0,0 +1,37 @@ +import * as Vue from 'vue' +import { useStore as useTanStackStore } from '@tanstack/vue-store' + +type StoreSource = { + get: () => T + subscribe: (listener: (value: T) => void) => { + unsubscribe: () => void + } +} + +// Vue Router hooks can run while functional components render, where there is +// no active effect scope for @tanstack/vue-store's useStore cleanup. Run the +// upstream hook in a scope tied to the current component's unmount lifecycle. +export function useStore>( + store: StoreSource, + selector: (state: NoInfer) => TSelected = (state) => + state as unknown as TSelected, + compare?: (a: TSelected, b: TSelected) => boolean, +): Readonly> { + const run = () => useTanStackStore(store, selector, compare) + + if (Vue.getCurrentScope()) { + return run() + } + + const instance = Vue.getCurrentInstance() + if (!instance) { + return run() + } + + const scope = Vue.effectScope(true) + const slice = scope.run(run)! + + Vue.onUnmounted(() => scope.stop(), instance) + + return slice +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a91aaea20f..bda40078c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12592,8 +12592,8 @@ importers: specifier: workspace:* version: link:../history '@tanstack/react-store': - specifier: ^0.9.3 - version: 0.9.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^0.11.0 + version: 0.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/router-core': specifier: workspace:* version: link:../router-core @@ -12880,8 +12880,8 @@ importers: version: 1.5.4(seroval@1.5.4) devDependencies: '@tanstack/store': - specifier: ^0.9.3 - version: 0.9.3 + specifier: ^0.11.0 + version: 0.11.0 '@types/node': specifier: 25.0.9 version: 25.0.9 @@ -13500,8 +13500,8 @@ importers: specifier: workspace:* version: link:../router-core '@tanstack/vue-store': - specifier: ^0.9.3 - version: 0.9.3(vue@3.5.25(typescript@6.0.2)) + specifier: ^0.11.0 + version: 0.11.0(vue@3.5.25(typescript@6.0.2)) '@vue/runtime-dom': specifier: ^3.5.25 version: 3.5.25 @@ -19318,11 +19318,11 @@ packages: peerDependencies: react: ^19.2.3 - '@tanstack/react-store@0.9.3': - resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} + '@tanstack/react-store@0.11.0': + resolution: {integrity: sha512-tX4YXh3PDkmpvGQWkWqKpzs/MSqbtuwY9dWdWhtV9Q50PmO+jOkUKIWIX4G85dwt7lxdHLXsiaEKPdKmC8F41w==} peerDependencies: - react: ^19.2.3 - react-dom: ^19.2.3 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@tanstack/react-virtual@3.13.0': resolution: {integrity: sha512-CchF0NlLIowiM2GxtsoKBkXA4uqSnY2KvnXo+kyUFD4a4ll6+J0qzoRsUPMwXV/H26lRsxgJIr/YmjYum2oEjg==} @@ -19352,8 +19352,8 @@ packages: peerDependencies: solid-js: 1.9.12 - '@tanstack/store@0.9.3': - resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} + '@tanstack/store@0.11.0': + resolution: {integrity: sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw==} '@tanstack/virtual-core@3.13.0': resolution: {integrity: sha512-NBKJP3OIdmZY3COJdWkSonr50FMVIi+aj5ZJ7hI/DTpEKg2RMfo/KvP8A3B/zOSpMgIe52B5E2yn7rryULzA6g==} @@ -19391,8 +19391,8 @@ packages: '@vue/composition-api': optional: true - '@tanstack/vue-store@0.9.3': - resolution: {integrity: sha512-YZb5SAR3f2kLt58Ip6gig2+z8vRAfSkJK30Bq7enZ7cG4epyygmRsbrrDMxvmoYSJu33CY5uJ6MvI74KGP0ZvQ==} + '@tanstack/vue-store@0.11.0': + resolution: {integrity: sha512-w1lE4D7oo6nIUhTQvL/RoTgcvoYJw+KvttWPNJd/Xu+MvNxMnjxFF/LgFp9vwmbNbiGhGCg3ZoPJptwjrfwNYw==} peerDependencies: '@vue/composition-api': ^1.2.1 vue: ^2.5.0 || ^3.0.0 @@ -32623,9 +32623,9 @@ snapshots: '@tanstack/query-core': 5.99.0 react: 19.2.3 - '@tanstack/react-store@0.9.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-store@0.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/store': 0.9.3 + '@tanstack/store': 0.11.0 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3) @@ -32661,7 +32661,7 @@ snapshots: '@tanstack/virtual-core': 3.13.12 solid-js: 1.9.12 - '@tanstack/store@0.9.3': {} + '@tanstack/store@0.11.0': {} '@tanstack/virtual-core@3.13.0': {} @@ -32715,9 +32715,9 @@ snapshots: vue: 3.5.25(typescript@6.0.2) vue-demi: 0.14.10(vue@3.5.25(typescript@6.0.2)) - '@tanstack/vue-store@0.9.3(vue@3.5.25(typescript@6.0.2))': + '@tanstack/vue-store@0.11.0(vue@3.5.25(typescript@6.0.2))': dependencies: - '@tanstack/store': 0.9.3 + '@tanstack/store': 0.11.0 vue: 3.5.25(typescript@6.0.2) vue-demi: 0.14.10(vue@3.5.25(typescript@6.0.2)) From f62402ec94553aca1cf4ffd1968de19b6c825975 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Wed, 15 Jul 2026 04:15:34 -0700 Subject: [PATCH 2/2] chore: update useStore to useSelector --- packages/react-router/src/Match.tsx | 18 ++--- packages/react-router/src/Matches.tsx | 10 +-- packages/react-router/src/Scripts.tsx | 14 ++-- packages/react-router/src/Transitioner.tsx | 6 +- .../react-router/src/headContentUtils.tsx | 26 ++++---- packages/react-router/src/link.tsx | 10 ++- packages/react-router/src/not-found.tsx | 6 +- packages/react-router/src/useCanGoBack.ts | 4 +- packages/react-router/src/useLocation.tsx | 4 +- packages/react-router/src/useMatch.tsx | 4 +- packages/react-router/src/useRouterState.tsx | 4 +- .../src/BaseTanStackRouterDevtoolsPanel.tsx | 2 +- packages/vue-router/src/Match.tsx | 28 +++----- packages/vue-router/src/Matches.tsx | 12 ++-- packages/vue-router/src/Scripts.tsx | 4 +- packages/vue-router/src/Transitioner.tsx | 6 +- packages/vue-router/src/headContentUtils.tsx | 4 +- packages/vue-router/src/link.tsx | 10 ++- packages/vue-router/src/not-found.tsx | 6 +- packages/vue-router/src/useCanGoBack.ts | 4 +- packages/vue-router/src/useLocation.tsx | 4 +- packages/vue-router/src/useMatch.tsx | 15 ++--- packages/vue-router/src/useRouterState.tsx | 4 +- .../src/{useStore.ts => useSelector.ts} | 11 ++-- .../vue-router/tests/useSelector.test.tsx | 66 +++++++++++++++++++ 25 files changed, 167 insertions(+), 115 deletions(-) rename packages/vue-router/src/{useStore.ts => useSelector.ts} (65%) create mode 100644 packages/vue-router/tests/useSelector.test.tsx diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx index 0dab08b4f6..8ebfa110f0 100644 --- a/packages/react-router/src/Match.tsx +++ b/packages/react-router/src/Match.tsx @@ -1,7 +1,7 @@ 'use client' import * as React from 'react' -import { useStore } from '@tanstack/react-store' +import { useSelector } from '@tanstack/react-store' import { createControlledPromise, getLocationChangeInfo, @@ -100,9 +100,11 @@ export const Match = React.memo(function MatchImpl({ invariant() } // eslint-disable-next-line react-hooks/rules-of-hooks - const resetKey = useStore(router.stores.loadedAt, (loadedAt) => loadedAt) + const resetKey = useSelector(router.stores.loadedAt) // eslint-disable-next-line react-hooks/rules-of-hooks - const match = useStore(matchStore, (value) => value, matchViewFieldsEqual) + const match = useSelector(matchStore, (value) => value, { + compare: matchViewFieldsEqual, + }) // eslint-disable-next-line react-hooks/rules-of-hooks const matchState = React.useMemo(() => { const routeId = match.routeId as string @@ -265,7 +267,7 @@ function OnRendered() { ParsedLocation | undefined >() // eslint-disable-next-line react-hooks/rules-of-hooks - const renderedLocationKey = useStore( + const renderedLocationKey = useSelector( router.stores.resolvedLocation, (resolvedLocation) => resolvedLocation?.state.__TSR_key, ) @@ -408,7 +410,7 @@ export const MatchInner = React.memo(function MatchInnerImpl({ invariant() } // eslint-disable-next-line react-hooks/rules-of-hooks - const match = useStore(matchStore, (value) => value) + const match = useSelector(matchStore) const routeId = match.routeId as string const route = router.routesById[routeId] as AnyRoute // eslint-disable-next-line react-hooks/rules-of-hooks @@ -561,17 +563,17 @@ export const Outlet = React.memo(function OutletImpl() { : emptyMatchStore // eslint-disable-next-line react-hooks/rules-of-hooks - ;[routeId, parentGlobalNotFound] = useStore( + ;[routeId, parentGlobalNotFound] = useSelector( parentMatchStore, (match): OutletMatchSelection => [ match?.routeId as string | undefined, match?.globalNotFound ?? false, ], - outletMatchSelectionEqual, + { compare: outletMatchSelectionEqual }, ) // eslint-disable-next-line react-hooks/rules-of-hooks - childMatchId = useStore(router.stores.matchesId, (ids) => { + childMatchId = useSelector(router.stores.matchesId, (ids) => { const index = ids.findIndex((id) => id === matchId) return ids[index + 1] }) diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx index 62d0fc42ae..aa72a0bd07 100644 --- a/packages/react-router/src/Matches.tsx +++ b/packages/react-router/src/Matches.tsx @@ -1,7 +1,7 @@ 'use client' import * as React from 'react' -import { useStore } from '@tanstack/react-store' +import { useSelector } from '@tanstack/react-store' import { rootRouteId } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' import { CatchBoundary, ErrorComponent } from './CatchBoundary' @@ -80,11 +80,11 @@ function MatchesInner() { const matchId = _isServer ? router.stores.firstId.get() : // eslint-disable-next-line react-hooks/rules-of-hooks - useStore(router.stores.firstId, (id) => id) + useSelector(router.stores.firstId) const resetKey = _isServer ? router.stores.loadedAt.get() : // eslint-disable-next-line react-hooks/rules-of-hooks - useStore(router.stores.loadedAt, (loadedAt) => loadedAt) + useSelector(router.stores.loadedAt) const matchComponent = matchId ? : null @@ -143,7 +143,7 @@ export function useMatchRoute() { if (!(isServer ?? router.isServer)) { // eslint-disable-next-line react-hooks/rules-of-hooks - useStore(router.stores.matchRouteDeps, (d) => d) + useSelector(router.stores.matchRouteDeps) } return React.useCallback( @@ -247,7 +247,7 @@ export function useMatches< } // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - return useStore( + return useSelector( router.stores.matches, // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static useStructuralSharing(opts, router), diff --git a/packages/react-router/src/Scripts.tsx b/packages/react-router/src/Scripts.tsx index 5285fbd8db..f3946a4ba3 100644 --- a/packages/react-router/src/Scripts.tsx +++ b/packages/react-router/src/Scripts.tsx @@ -1,4 +1,4 @@ -import { useStore } from '@tanstack/react-store' +import { useSelector } from '@tanstack/react-store' import { deepEqual } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' import { Asset } from './Asset' @@ -74,13 +74,13 @@ export const Scripts = () => { } // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - const assetScripts = useStore( - router.stores.matches, - getAssetScripts, - deepEqual, - ) + const assetScripts = useSelector(router.stores.matches, getAssetScripts, { + compare: deepEqual, + }) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - const scripts = useStore(router.stores.matches, getScripts, deepEqual) + const scripts = useSelector(router.stores.matches, getScripts, { + compare: deepEqual, + }) return renderScripts(router, scripts, assetScripts) } diff --git a/packages/react-router/src/Transitioner.tsx b/packages/react-router/src/Transitioner.tsx index bf945571a6..b3ea2d36c6 100644 --- a/packages/react-router/src/Transitioner.tsx +++ b/packages/react-router/src/Transitioner.tsx @@ -1,7 +1,7 @@ 'use client' import * as React from 'react' -import { batch, useStore } from '@tanstack/react-store' +import { batch, useSelector } from '@tanstack/react-store' import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core' import { useLayoutEffect, usePrevious } from './utils' import { useRouter } from './useRouter' @@ -12,8 +12,8 @@ export function Transitioner() { const [isTransitioning, setIsTransitioning] = React.useState(false) // Track pending state changes - const isLoading = useStore(router.stores.isLoading, (value) => value) - const hasPending = useStore(router.stores.hasPending, (value) => value) + const isLoading = useSelector(router.stores.isLoading) + const hasPending = useSelector(router.stores.hasPending) const previousIsLoading = usePrevious(isLoading) diff --git a/packages/react-router/src/headContentUtils.tsx b/packages/react-router/src/headContentUtils.tsx index 5ed35bd1ce..ae09b3f68b 100644 --- a/packages/react-router/src/headContentUtils.tsx +++ b/packages/react-router/src/headContentUtils.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useStore } from '@tanstack/react-store' +import { useSelector } from '@tanstack/react-store' import { appendUniqueUserTags, deepEqual, @@ -204,14 +204,14 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => { } // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - const routeMeta = useStore( + const routeMeta = useSelector( router.stores.matches, (matches) => { return matches .map((match) => match.meta) .filter((meta) => meta !== undefined) }, - deepEqual, + { compare: deepEqual }, ) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static @@ -287,7 +287,7 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => { }, [routeMeta, nonce]) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - const links = useStore( + const links = useSelector( router.stores.matches, (matches) => { const constructed = matches @@ -303,11 +303,11 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => { return constructed }, - deepEqual, + { compare: deepEqual }, ) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - const manifestCssTags = useStore( + const manifestCssTags = useSelector( router.stores.matches, (matches) => { const manifest = router.ssr?.manifest @@ -349,11 +349,11 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => { return tags }, - deepEqual, + { compare: deepEqual }, ) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - const preloadLinks = useStore( + const preloadLinks = useSelector( router.stores.matches, (matches) => { const preloadLinks: Array = [] @@ -377,11 +377,11 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => { return preloadLinks }, - deepEqual, + { compare: deepEqual }, ) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - const styles = useStore( + const styles = useSelector( router.stores.matches, (matches) => { return matches @@ -396,11 +396,11 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => { children: children as string | undefined, })) satisfies Array }, - deepEqual, + { compare: deepEqual }, ) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - const headScripts: Array = useStore( + const headScripts: Array = useSelector( router.stores.matches, (matches) => { return matches @@ -415,7 +415,7 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => { children: children as string | undefined, })) satisfies Array }, - deepEqual, + { compare: deepEqual }, ) const tags: Array = [] diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx index eddc0ac0b3..3537650b2d 100644 --- a/packages/react-router/src/link.tsx +++ b/packages/react-router/src/link.tsx @@ -1,7 +1,7 @@ 'use client' import * as React from 'react' -import { useStore } from '@tanstack/react-store' +import { useSelector } from '@tanstack/react-store' import { flushSync } from 'react-dom' import { deepEqual, @@ -400,11 +400,9 @@ export function useLinkProps< ) // eslint-disable-next-line react-hooks/rules-of-hooks - const currentLocation = useStore( - router.stores.location, - (l) => l, - (prev, next) => prev.href === next.href, - ) + const currentLocation = useSelector(router.stores.location, (l) => l, { + compare: (prev, next) => prev.href === next.href, + }) // eslint-disable-next-line react-hooks/rules-of-hooks const next = React.useMemo(() => { diff --git a/packages/react-router/src/not-found.tsx b/packages/react-router/src/not-found.tsx index ce3a04c748..f932b35bb7 100644 --- a/packages/react-router/src/not-found.tsx +++ b/packages/react-router/src/not-found.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { isNotFound } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' -import { useStore } from '@tanstack/react-store' +import { useSelector } from '@tanstack/react-store' import { CatchBoundary } from './CatchBoundary' import { useRouter } from './useRouter' import type { ErrorInfo } from 'react' @@ -44,12 +44,12 @@ export function CatchNotFound(props: { // TODO: Some way for the user to programmatically reset the not-found boundary? // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - const pathname = useStore( + const pathname = useSelector( router.stores.location, (location) => location.pathname, ) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - const status = useStore(router.stores.status, (status) => status) + const status = useSelector(router.stores.status) const resetKey = `not-found-${pathname}-${status}` return ( diff --git a/packages/react-router/src/useCanGoBack.ts b/packages/react-router/src/useCanGoBack.ts index a20f947f43..2a8ce612b1 100644 --- a/packages/react-router/src/useCanGoBack.ts +++ b/packages/react-router/src/useCanGoBack.ts @@ -1,4 +1,4 @@ -import { useStore } from '@tanstack/react-store' +import { useSelector } from '@tanstack/react-store' import { isServer } from '@tanstack/router-core/isServer' import { useRouter } from './useRouter' @@ -10,7 +10,7 @@ export function useCanGoBack() { } // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - return useStore( + return useSelector( router.stores.location, (location) => location.state.__TSR_index !== 0, ) diff --git a/packages/react-router/src/useLocation.tsx b/packages/react-router/src/useLocation.tsx index 5c8c7c8d7e..5fa39fa92e 100644 --- a/packages/react-router/src/useLocation.tsx +++ b/packages/react-router/src/useLocation.tsx @@ -1,6 +1,6 @@ 'use client' -import { useStore } from '@tanstack/react-store' +import { useSelector } from '@tanstack/react-store' import { isServer } from '@tanstack/router-core/isServer' import { useRouter } from './useRouter' import { useStructuralSharing } from './useMatch' @@ -60,7 +60,7 @@ export function useLocation< } // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - return useStore( + return useSelector( router.stores.location, // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static useStructuralSharing(opts, router), diff --git a/packages/react-router/src/useMatch.tsx b/packages/react-router/src/useMatch.tsx index 6c1d03421d..5aa5973598 100644 --- a/packages/react-router/src/useMatch.tsx +++ b/packages/react-router/src/useMatch.tsx @@ -1,7 +1,7 @@ 'use client' import * as React from 'react' -import { useStore } from '@tanstack/react-store' +import { useSelector } from '@tanstack/react-store' import { invariant, replaceEqualDeep } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' import { dummyMatchContext, matchContext } from './matchContext' @@ -179,7 +179,7 @@ export function useMatch< useStructuralSharing(opts, router) // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - const matchSelection = useStore(matchStore ?? dummyStore, (match) => + const matchSelection = useSelector(matchStore ?? dummyStore, (match) => match ? selector(match as any) : dummyStore, ) diff --git a/packages/react-router/src/useRouterState.tsx b/packages/react-router/src/useRouterState.tsx index 1e281645af..ec2a9e6c34 100644 --- a/packages/react-router/src/useRouterState.tsx +++ b/packages/react-router/src/useRouterState.tsx @@ -1,6 +1,6 @@ 'use client' -import { useStore } from '@tanstack/react-store' +import { useSelector } from '@tanstack/react-store' import { isServer } from '@tanstack/router-core/isServer' import { useRouter } from './useRouter' import { useStructuralSharing } from './useMatch' @@ -68,7 +68,7 @@ export function useRouterState< } // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static - return useStore( + return useSelector( router.stores.__store, // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static useStructuralSharing(opts, router), diff --git a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx index 79566656b0..594dce3acf 100644 --- a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx +++ b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx @@ -268,7 +268,7 @@ export const BaseTanStackRouterDevtoolsPanel = const styles = useStyles() const { className, style, ...otherPanelProps } = panelProps - // useStore(router.stores.__store) + // useSelector(router.stores.__store) const [currentTab, setCurrentTab] = useLocalStorage< 'routes' | 'matches' | 'history' diff --git a/packages/vue-router/src/Match.tsx b/packages/vue-router/src/Match.tsx index a029c14194..311133f0ef 100644 --- a/packages/vue-router/src/Match.tsx +++ b/packages/vue-router/src/Match.tsx @@ -8,7 +8,7 @@ import { rootRouteId, } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' -import { useStore } from './useStore' +import { useSelector } from './useSelector' import { CatchBoundary, ErrorComponent } from './CatchBoundary' import { ClientOnly } from './ClientOnly' import { useRouter } from './useRouter' @@ -57,16 +57,13 @@ export const Match = Vue.defineComponent({ // Single stable store subscription — getRouteMatchStore returns a // cached computed store that resolves routeId → current match state // through the signal graph. No bridge needed. - const activeMatch = useStore( - router.stores.getRouteMatchStore(routeId), - (value) => value, - ) - const isPendingMatchRef = useStore( + const activeMatch = useSelector(router.stores.getRouteMatchStore(routeId)) + const isPendingMatchRef = useSelector( router.stores.pendingRouteIds, (pendingRouteIds) => Boolean(pendingRouteIds[routeId]), - Object.is, + { compare: Object.is }, ) - const loadedAt = useStore(router.stores.loadedAt, (value) => value) + const loadedAt = useSelector(router.stores.loadedAt) const matchData = Vue.computed(() => { const match = activeMatch.value @@ -250,7 +247,7 @@ const OnRendered = Vue.defineComponent({ setup() { const router = useRouter() - const location = useStore( + const location = useSelector( router.stores.resolvedLocation, (resolvedLocation) => resolvedLocation?.state.__TSR_key, ) @@ -294,10 +291,7 @@ export const MatchInner = Vue.defineComponent({ // Use routeId from context (provided by parent Match) — stable string. const routeId = Vue.inject(routeIdContext)! - const activeMatch = useStore( - router.stores.getRouteMatchStore(routeId), - (value) => value, - ) + const activeMatch = useSelector(router.stores.getRouteMatchStore(routeId)) // Combined selector for match state AND remount key // This ensures both are computed in the same selector call with consistent data @@ -497,9 +491,8 @@ export const Outlet = Vue.defineComponent({ } // Parent state via stable routeId store — single subscription - const parentMatch = useStore( + const parentMatch = useSelector( router.stores.getRouteMatchStore(parentRouteId), - (v) => v, ) const route = Vue.computed(() => @@ -515,10 +508,7 @@ export const Outlet = Vue.defineComponent({ // Child match lookup: read the child matchId from the shared derived // map (one reactive node for the whole tree), then grab match state // directly from the pool. - const childMatchIdMap = useStore( - router.stores.childMatchIdByRouteId, - (v) => v, - ) + const childMatchIdMap = useSelector(router.stores.childMatchIdByRouteId) const childMatchData = Vue.computed(() => { const childId = childMatchIdMap.value[parentRouteId] diff --git a/packages/vue-router/src/Matches.tsx b/packages/vue-router/src/Matches.tsx index 273e57e998..556569e825 100644 --- a/packages/vue-router/src/Matches.tsx +++ b/packages/vue-router/src/Matches.tsx @@ -1,6 +1,6 @@ import * as Vue from 'vue' import { isServer } from '@tanstack/router-core/isServer' -import { useStore } from './useStore' +import { useSelector } from './useSelector' import { CatchBoundary } from './CatchBoundary' import { useRouter } from './useRouter' import { useTransitionerSetup } from './Transitioner' @@ -99,8 +99,8 @@ const MatchesInner = Vue.defineComponent({ setup() { const router = useRouter() - const matchId = useStore(router.stores.firstId, (id) => id) - const resetKey = useStore(router.stores.loadedAt, (loadedAt) => loadedAt) + const matchId = useSelector(router.stores.firstId) + const resetKey = useSelector(router.stores.loadedAt) // Create a ref for the match id to provide const matchIdRef = Vue.computed(() => matchId.value) @@ -152,7 +152,7 @@ export type UseMatchRouteOptions< export function useMatchRoute() { const router = useRouter() - const routerState = useStore(router.stores.matchRouteDeps, (value) => value) + const routerState = useSelector(router.stores.matchRouteDeps) return < const TFrom extends string = string, @@ -249,7 +249,7 @@ export const MatchRoute = Vue.defineComponent({ }, setup(props, { slots }) { const router = useRouter() - const status = useStore( + const status = useSelector( router.stores.matchRouteDeps, (value) => value.status, ) @@ -293,7 +293,7 @@ export function useMatches< opts?: UseMatchesBaseOptions, ): Vue.Ref> { const router = useRouter() - return useStore(router.stores.matches, (matches) => { + return useSelector(router.stores.matches, (matches) => { return opts?.select ? opts.select(matches as Array>) : (matches as any) diff --git a/packages/vue-router/src/Scripts.tsx b/packages/vue-router/src/Scripts.tsx index 221eec5a5c..1c1c115ff4 100644 --- a/packages/vue-router/src/Scripts.tsx +++ b/packages/vue-router/src/Scripts.tsx @@ -1,7 +1,7 @@ import * as Vue from 'vue' import { isServer } from '@tanstack/router-core/isServer' import { Asset } from './Asset' -import { useStore } from './useStore' +import { useSelector } from './useSelector' import { useRouter } from './useRouter' import type { RouterManagedTag } from '@tanstack/router-core' @@ -17,7 +17,7 @@ export const Scripts = Vue.defineComponent({ setup() { const router = useRouter() const nonce = router.options.ssr?.nonce - const matches = useStore(router.stores.matches, (value) => value) + const matches = useSelector(router.stores.matches) const getAssetScripts = (matches: Array) => { const assetScripts: Array = [] diff --git a/packages/vue-router/src/Transitioner.tsx b/packages/vue-router/src/Transitioner.tsx index a0fa3f8b56..3d2cd6b24e 100644 --- a/packages/vue-router/src/Transitioner.tsx +++ b/packages/vue-router/src/Transitioner.tsx @@ -2,7 +2,7 @@ import * as Vue from 'vue' import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' import { batch } from '@tanstack/vue-store' -import { useStore } from './useStore' +import { useSelector } from './useSelector' import { useRouter } from './useRouter' import { usePrevious } from './utils' @@ -27,13 +27,13 @@ export function useTransitionerSetup() { return } - const isLoading = useStore(router.stores.isLoading, (value) => value) + const isLoading = useSelector(router.stores.isLoading) // Track if we're in a transition - using a ref to track async transitions const isTransitioning = Vue.ref(false) // Track pending state changes - const hasPending = useStore(router.stores.hasPending, (value) => value) + const hasPending = useSelector(router.stores.hasPending) const previousIsLoading = usePrevious(() => isLoading.value) diff --git a/packages/vue-router/src/headContentUtils.tsx b/packages/vue-router/src/headContentUtils.tsx index 20a4945260..fc07dbede8 100644 --- a/packages/vue-router/src/headContentUtils.tsx +++ b/packages/vue-router/src/headContentUtils.tsx @@ -6,7 +6,7 @@ import { getScriptPreloadAttrs, resolveManifestCssLink, } from '@tanstack/router-core' -import { useStore } from './useStore' +import { useSelector } from './useSelector' import { useRouter } from './useRouter' import type { AssetCrossOriginConfig, @@ -15,7 +15,7 @@ import type { export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => { const router = useRouter() - const matches = useStore(router.stores.matches, (value) => value) + const matches = useSelector(router.stores.matches) const meta = Vue.computed>(() => { const resultMeta: Array = [] diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index 0c5a8057f3..1e757f5b59 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -9,7 +9,7 @@ import { } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' -import { useStore } from './useStore' +import { useSelector } from './useSelector' import { useRouter } from './useRouter' import { useIntersectionObserver } from './utils' @@ -219,11 +219,9 @@ export function useLinkProps< ) as unknown as LinkHTMLAttributes } - const currentLocation = useStore( - router.stores.location, - (l) => l, - (prev, next) => prev.href === next.href, - ) + const currentLocation = useSelector(router.stores.location, (l) => l, { + compare: (prev, next) => prev.href === next.href, + }) const next = Vue.computed(() => { // Rebuild when inherited search/hash or the current route context changes. diff --git a/packages/vue-router/src/not-found.tsx b/packages/vue-router/src/not-found.tsx index 5eb26583f1..c4afd406fb 100644 --- a/packages/vue-router/src/not-found.tsx +++ b/packages/vue-router/src/not-found.tsx @@ -1,6 +1,6 @@ import * as Vue from 'vue' import { isNotFound } from '@tanstack/router-core' -import { useStore } from './useStore' +import { useSelector } from './useSelector' import { CatchBoundary } from './CatchBoundary' import { useRouter } from './useRouter' import type { ErrorComponentProps, NotFoundError } from '@tanstack/router-core' @@ -12,11 +12,11 @@ export function CatchNotFound(props: { }) { const router = useRouter() // TODO: Some way for the user to programmatically reset the not-found boundary? - const pathname = useStore( + const pathname = useSelector( router.stores.location, (location) => location.pathname, ) - const status = useStore(router.stores.status, (value) => value) + const status = useSelector(router.stores.status) // Create a function that returns a VNode to match the SyncRouteComponent signature const errorComponentFn = (componentProps: ErrorComponentProps) => { diff --git a/packages/vue-router/src/useCanGoBack.ts b/packages/vue-router/src/useCanGoBack.ts index 0b363f0923..e5f1f88ad8 100644 --- a/packages/vue-router/src/useCanGoBack.ts +++ b/packages/vue-router/src/useCanGoBack.ts @@ -1,9 +1,9 @@ -import { useStore } from './useStore' +import { useSelector } from './useSelector' import { useRouter } from './useRouter' export function useCanGoBack() { const router = useRouter() - return useStore( + return useSelector( router.stores.location, (location) => location.state.__TSR_index !== 0, ) diff --git a/packages/vue-router/src/useLocation.tsx b/packages/vue-router/src/useLocation.tsx index b21332fe5b..5791ce5cba 100644 --- a/packages/vue-router/src/useLocation.tsx +++ b/packages/vue-router/src/useLocation.tsx @@ -1,4 +1,4 @@ -import { useStore } from './useStore' +import { useSelector } from './useSelector' import { useRouter } from './useRouter' import type { AnyRouter, @@ -25,7 +25,7 @@ export function useLocation< opts?: UseLocationBaseOptions, ): Vue.Ref> { const router = useRouter() - return useStore(router.stores.location, (location) => + return useSelector(router.stores.location, (location) => opts?.select ? opts.select(location) : location, ) as Vue.Ref> } diff --git a/packages/vue-router/src/useMatch.tsx b/packages/vue-router/src/useMatch.tsx index 72e0b656e4..80e553b8b0 100644 --- a/packages/vue-router/src/useMatch.tsx +++ b/packages/vue-router/src/useMatch.tsx @@ -1,7 +1,7 @@ import * as Vue from 'vue' import { invariant } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' -import { useStore } from './useStore' +import { useSelector } from './useSelector' import { injectDummyPendingMatch, injectPendingMatch, @@ -124,17 +124,14 @@ export function useMatch< // routeId case: single subscription via per-routeId computed store. // The store reference is stable (cached by routeId). const matchStore = router.stores.getRouteMatchStore(opts.from) - match = useStore(matchStore, (value) => value) + match = useSelector(matchStore) } else { // matchId case: use routeId from context for stable store lookup. // The routeId is provided by the nearest Match component and doesn't // change for the component's lifetime, so the store is stable. const nearestRouteId = Vue.inject(routeIdContext) if (nearestRouteId) { - match = useStore( - router.stores.getRouteMatchStore(nearestRouteId), - (value) => value, - ) + match = useSelector(router.stores.getRouteMatchStore(nearestRouteId)) } else { // No route context — will fall through to error handling below match = Vue.ref(undefined) as Readonly> @@ -142,12 +139,12 @@ export function useMatch< } const hasPendingRouteMatch = opts.from - ? useStore(router.stores.pendingRouteIds, (ids) => ids) + ? useSelector(router.stores.pendingRouteIds) : undefined - const isTransitioning = useStore( + const isTransitioning = useSelector( router.stores.isTransitioning, (value) => value, - Object.is, + { compare: Object.is }, ) const result = Vue.computed(() => { diff --git a/packages/vue-router/src/useRouterState.tsx b/packages/vue-router/src/useRouterState.tsx index 1749f15442..a010fa22a9 100644 --- a/packages/vue-router/src/useRouterState.tsx +++ b/packages/vue-router/src/useRouterState.tsx @@ -1,6 +1,6 @@ import * as Vue from 'vue' import { isServer } from '@tanstack/router-core/isServer' -import { useStore } from './useStore' +import { useSelector } from './useSelector' import { useRouter } from './useRouter' import type { AnyRouter, @@ -50,7 +50,7 @@ export function useRouterState< > } - return useStore(router.stores.__store, (state) => { + return useSelector(router.stores.__store, (state) => { if (opts?.select) return opts.select(state) return state diff --git a/packages/vue-router/src/useStore.ts b/packages/vue-router/src/useSelector.ts similarity index 65% rename from packages/vue-router/src/useStore.ts rename to packages/vue-router/src/useSelector.ts index 77d965211d..52e2849165 100644 --- a/packages/vue-router/src/useStore.ts +++ b/packages/vue-router/src/useSelector.ts @@ -1,5 +1,6 @@ import * as Vue from 'vue' -import { useStore as useTanStackStore } from '@tanstack/vue-store' +import { useSelector as useTanStackSelector } from '@tanstack/vue-store' +import type { UseSelectorOptions } from '@tanstack/vue-store' type StoreSource = { get: () => T @@ -9,15 +10,15 @@ type StoreSource = { } // Vue Router hooks can run while functional components render, where there is -// no active effect scope for @tanstack/vue-store's useStore cleanup. Run the +// no active effect scope for @tanstack/vue-store's useSelector cleanup. Run the // upstream hook in a scope tied to the current component's unmount lifecycle. -export function useStore>( +export function useSelector>( store: StoreSource, selector: (state: NoInfer) => TSelected = (state) => state as unknown as TSelected, - compare?: (a: TSelected, b: TSelected) => boolean, + options?: UseSelectorOptions, ): Readonly> { - const run = () => useTanStackStore(store, selector, compare) + const run = () => useTanStackSelector(store, selector, options) if (Vue.getCurrentScope()) { return run() diff --git a/packages/vue-router/tests/useSelector.test.tsx b/packages/vue-router/tests/useSelector.test.tsx new file mode 100644 index 0000000000..97578d4ed8 --- /dev/null +++ b/packages/vue-router/tests/useSelector.test.tsx @@ -0,0 +1,66 @@ +import * as Vue from 'vue' +import { render } from '@testing-library/vue' +import { describe, expect, it, vi } from 'vitest' +import { useSelector } from '../src/useSelector' + +type State = { count: number } + +function createStore(initialState: State) { + let state = initialState + const listeners = new Set<(state: State) => void>() + const unsubscribe = vi.fn((listener: (state: State) => void) => { + listeners.delete(listener) + }) + + return { + store: { + get: () => state, + subscribe: vi.fn((listener: (state: State) => void) => { + listeners.add(listener) + return { unsubscribe: () => unsubscribe(listener) } + }), + }, + setState(nextState: State) { + state = nextState + listeners.forEach((listener) => listener(state)) + }, + unsubscribe, + } +} + +describe('useSelector', () => { + it('cleans up a functional component subscription on unmount', () => { + const { store, unsubscribe } = createStore({ count: 0 }) + const Component = () => { + const count = useSelector(store, (state) => state.count) + return
{count.value}
+ } + + const view = render(Component) + view.unmount() + + expect(store.subscribe).toHaveBeenCalledOnce() + expect(unsubscribe).toHaveBeenCalledOnce() + }) + + it('passes comparator options to the upstream selector', () => { + const { store, setState } = createStore({ count: 0 }) + let selected: Readonly> | undefined + const Component = Vue.defineComponent({ + setup() { + const selection = useSelector(store, (state) => state, { + compare: (previous, next) => previous.count === next.count, + }) + selected = selection + return () =>
{selection.value.count}
+ }, + }) + + const view = render(Component) + const initialSelection = selected!.value + setState({ count: 0 }) + + expect(selected!.value).toBe(initialSelection) + view.unmount() + }) +})