> {
+ const select = () => useTanStackSelector(store, selector, options)
+
+ if (Vue.getCurrentScope()) {
+ return select()
+ }
+
+ const instance = Vue.getCurrentInstance()
+ if (!instance) {
+ return select()
+ }
+
+ let state = functionalSelectorStates.get(instance)
+ if (!state) {
+ const functionalState = { scope: Vue.effectScope(true) }
+ state = functionalState
+ functionalSelectorStates.set(instance, functionalState)
+
+ Vue.onBeforeUpdate(() => {
+ functionalState.scope.stop()
+ functionalState.scope = Vue.effectScope(true)
+ }, instance)
+ Vue.onUnmounted(() => {
+ functionalState.scope.stop()
+ functionalSelectorStates.delete(instance)
+ }, instance)
+ }
+
+ return state.scope.run(select)!
+}
diff --git a/packages/vue-router/tests/not-found.test.tsx b/packages/vue-router/tests/not-found.test.tsx
index 3cb4a78ec3..dffba3e05a 100644
--- a/packages/vue-router/tests/not-found.test.tsx
+++ b/packages/vue-router/tests/not-found.test.tsx
@@ -1,4 +1,4 @@
-import { afterEach, beforeEach, expect, test } from 'vitest'
+import { afterEach, beforeEach, expect, test, vi } from 'vitest'
import { cleanup, render, screen } from '@testing-library/vue'
import {
@@ -126,6 +126,36 @@ test.each([
},
)
+test('does not recreate not-found subscriptions when the location changes', async () => {
+ const rootRoute = createRootRoute({
+ component: Outlet,
+ notFoundComponent: () => (
+ Root Not Found
+ ),
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Index,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute]),
+ history,
+ })
+ const subscribe = vi.spyOn(router.stores.location, 'subscribe')
+
+ render()
+ await router.load()
+
+ expect(subscribe).toHaveBeenCalledOnce()
+
+ await router.navigate({ to: '/missing-one' as any })
+ await screen.findByTestId('root-not-found')
+ await router.navigate({ to: '/missing-two' as any })
+
+ expect(subscribe).toHaveBeenCalledOnce()
+})
+
test('defaultNotFoundComponent and notFoundComponent receives data props via spread operator', async () => {
const isCustomData = (data: unknown): data is typeof customData => {
return 'message' in (data as typeof customData)
diff --git a/packages/vue-router/tests/useSelector.test.tsx b/packages/vue-router/tests/useSelector.test.tsx
new file mode 100644
index 0000000000..92e6bd3b58
--- /dev/null
+++ b/packages/vue-router/tests/useSelector.test.tsx
@@ -0,0 +1,64 @@
+import * as Vue from 'vue'
+import { render } from '@testing-library/vue'
+import { expect, test, vi } from 'vitest'
+import { useSelector } from '../src/useSelector'
+
+function createStore(initialValue: number) {
+ let value = initialValue
+ const listeners = new Set<(value: number) => void>()
+ const unsubscribe = vi.fn((listener: (value: number) => void) => {
+ listeners.delete(listener)
+ })
+ const subscribe = vi.fn((listener: (value: number) => void) => {
+ listeners.add(listener)
+ return { unsubscribe: () => unsubscribe(listener) }
+ })
+
+ return {
+ store: {
+ get: () => value,
+ subscribe,
+ },
+ setValue(nextValue: number) {
+ value = nextValue
+ listeners.forEach((listener) => listener(value))
+ },
+ listenerCount: () => listeners.size,
+ subscribe,
+ unsubscribe,
+ }
+}
+
+test('replaces functional component subscriptions on rerender', async () => {
+ const source = createStore(0)
+ const label = Vue.ref('first')
+ const Component = () => {
+ const value = useSelector(source.store)
+ return {`${label.value}:${value.value}`}
+ }
+
+ const view = render(Component)
+
+ expect(source.subscribe).toHaveBeenCalledOnce()
+ expect(source.listenerCount()).toBe(1)
+
+ label.value = 'second'
+ await Vue.nextTick()
+
+ expect(source.subscribe).toHaveBeenCalledTimes(2)
+ expect(source.unsubscribe).toHaveBeenCalledOnce()
+ expect(source.listenerCount()).toBe(1)
+
+ source.setValue(1)
+ await Vue.nextTick()
+
+ expect(view.getByText('second:1')).toBeInTheDocument()
+ expect(source.subscribe).toHaveBeenCalledTimes(3)
+ expect(source.unsubscribe).toHaveBeenCalledTimes(2)
+ expect(source.listenerCount()).toBe(1)
+
+ view.unmount()
+
+ expect(source.unsubscribe).toHaveBeenCalledTimes(3)
+ expect(source.listenerCount()).toBe(0)
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 430cfcfaa5..c843f88cda 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -13398,8 +13398,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.1
+ version: 0.11.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tanstack/router-core':
specifier: workspace:*
version: link:../router-core
@@ -13689,8 +13689,8 @@ importers:
version: 1.6.2(seroval@1.6.2)
devDependencies:
'@tanstack/store':
- specifier: ^0.9.3
- version: 0.9.3
+ specifier: ^0.11.1
+ version: 0.11.1
'@types/node':
specifier: 25.0.9
version: 25.0.9
@@ -14315,8 +14315,8 @@ importers:
specifier: workspace:*
version: link:../router-core
'@tanstack/vue-store':
- specifier: ^0.9.3
- version: 0.9.3(vue@3.5.25(typescript@7.0.2))
+ specifier: ^0.11.1
+ version: 0.11.1(vue@3.5.25(typescript@7.0.2))
'@vue/runtime-dom':
specifier: ^3.5.25
version: 3.5.25
@@ -20219,11 +20219,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.1':
+ resolution: {integrity: sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ==}
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==}
@@ -20253,8 +20253,8 @@ packages:
peerDependencies:
solid-js: 1.9.12
- '@tanstack/store@0.9.3':
- resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==}
+ '@tanstack/store@0.11.1':
+ resolution: {integrity: sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA==}
'@tanstack/virtual-core@3.13.0':
resolution: {integrity: sha512-NBKJP3OIdmZY3COJdWkSonr50FMVIi+aj5ZJ7hI/DTpEKg2RMfo/KvP8A3B/zOSpMgIe52B5E2yn7rryULzA6g==}
@@ -20292,8 +20292,8 @@ packages:
'@vue/composition-api':
optional: true
- '@tanstack/vue-store@0.9.3':
- resolution: {integrity: sha512-YZb5SAR3f2kLt58Ip6gig2+z8vRAfSkJK30Bq7enZ7cG4epyygmRsbrrDMxvmoYSJu33CY5uJ6MvI74KGP0ZvQ==}
+ '@tanstack/vue-store@0.11.1':
+ resolution: {integrity: sha512-0YmYwbiCQKhzfFTLpHybjoAVUh3GkQVYGguvGTzk/0wx0oWuni60huvRs1RqqRLJH6nig3PTYuLuQQ+X5s05gA==}
peerDependencies:
'@vue/composition-api': ^1.2.1
vue: ^2.5.0 || ^3.0.0
@@ -33685,9 +33685,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.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
- '@tanstack/store': 0.9.3
+ '@tanstack/store': 0.11.1
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
use-sync-external-store: 1.6.0(react@19.2.3)
@@ -33723,7 +33723,7 @@ snapshots:
'@tanstack/virtual-core': 3.13.12
solid-js: 1.9.12
- '@tanstack/store@0.9.3': {}
+ '@tanstack/store@0.11.1': {}
'@tanstack/virtual-core@3.13.0': {}
@@ -33791,9 +33791,9 @@ snapshots:
vue: 3.5.25(typescript@7.0.2)
vue-demi: 0.14.10(vue@3.5.25(typescript@7.0.2))
- '@tanstack/vue-store@0.9.3(vue@3.5.25(typescript@7.0.2))':
+ '@tanstack/vue-store@0.11.1(vue@3.5.25(typescript@7.0.2))':
dependencies:
- '@tanstack/store': 0.9.3
+ '@tanstack/store': 0.11.1
vue: 3.5.25(typescript@7.0.2)
vue-demi: 0.14.10(vue@3.5.25(typescript@7.0.2))
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index aca73336f9..53aee7e62d 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,5 +1,9 @@
cleanupUnusedCatalogs: true
minimumReleaseAge: 1440
+minimumReleaseAgeExclude:
+ - '@tanstack/react-store@0.11.1'
+ - '@tanstack/store@0.11.1'
+ - '@tanstack/vue-store@0.11.1'
linkWorkspacePackages: true
preferWorkspacePackages: true
blockExoticSubdeps: true