diff --git a/.changeset/kind-garlics-melt.md b/.changeset/kind-garlics-melt.md
new file mode 100644
index 00000000..03c0c95f
--- /dev/null
+++ b/.changeset/kind-garlics-melt.md
@@ -0,0 +1,5 @@
+---
+'@tanstack/react-store': patch
+---
+
+Fix selections retained by mounted subscriptions and reduce selector callback allocation overhead. Use compact private selection cache fields to reduce consumer bundle size.
diff --git a/.changeset/pre.json b/.changeset/pre.json
new file mode 100644
index 00000000..9a89b8a3
--- /dev/null
+++ b/.changeset/pre.json
@@ -0,0 +1,4 @@
+{
+ "mode": "pre",
+ "tag": "alpha"
+}
diff --git a/.changeset/pre/grumpy-hairs-shop.md b/.changeset/pre/grumpy-hairs-shop.md
new file mode 100644
index 00000000..4b417a31
--- /dev/null
+++ b/.changeset/pre/grumpy-hairs-shop.md
@@ -0,0 +1,7 @@
+---
+'@tanstack/vue-store': major
+---
+
+Drop Vue 2 support from `@tanstack/vue-store`. The adapter now requires Vue 3 (`vue: ^3.0.0`) and imports Vue APIs and types directly, removing its `vue-demi` dependency and optional `@vue/composition-api` peer dependency.
+
+Vue 2 applications must upgrade to Vue 3 or keep using a previous version of `@tanstack/vue-store`. The adapter's public hooks and their behavior are unchanged for Vue 3 applications.
diff --git a/.changeset/pre/react-store-use-selector-single-ref.md b/.changeset/pre/react-store-use-selector-single-ref.md
new file mode 100644
index 00000000..c10eed47
--- /dev/null
+++ b/.changeset/pre/react-store-use-selector-single-ref.md
@@ -0,0 +1,7 @@
+---
+'@tanstack/react-store': major
+---
+
+`@tanstack/react-store` now requires React 18 or newer (`peerDependencies` are `react` and `react-dom` `^18.0.0 || ^19.0.0`); support for React 16.8 and 17 has been dropped.
+
+`useSelector` builds on React's built-in `useSyncExternalStore` with a single memoized selection ref instead of the `use-sync-external-store/shim/with-selector` helper: fewer hook slots and allocations per subscribed component, no per-component passive effect, and the `use-sync-external-store` dependency is gone from consumer bundles. The public API and selection semantics of `useSelector`, `useAtom`, `_useStore` and `useStore` are unchanged.
diff --git a/.github/renovate.json b/.github/renovate.json
index eecf09c7..cf5493ac 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json
@@ -28,8 +28,6 @@
"typescript50",
"typescript51",
"typescript52",
- "typescript53",
- "vue2",
- "vue2.7"
+ "typescript53"
]
}
diff --git a/docs/framework/react/reference/functions/useSelector.md b/docs/framework/react/reference/functions/useSelector.md
index 1ea0f378..3ab461bc 100644
--- a/docs/framework/react/reference/functions/useSelector.md
+++ b/docs/framework/react/reference/functions/useSelector.md
@@ -10,7 +10,7 @@ function useSelector(
options?): TSelected;
```
-Defined in: [packages/react-store/src/useSelector.ts:43](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSelector.ts#L43)
+Defined in: [packages/react-store/src/useSelector.ts:94](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSelector.ts#L94)
Selects a slice of state from an atom or store and subscribes the component
to that selection.
diff --git a/docs/framework/react/reference/interfaces/UseSelectorOptions.md b/docs/framework/react/reference/interfaces/UseSelectorOptions.md
index 67e1e1e6..dfcd3401 100644
--- a/docs/framework/react/reference/interfaces/UseSelectorOptions.md
+++ b/docs/framework/react/reference/interfaces/UseSelectorOptions.md
@@ -3,7 +3,7 @@ id: UseSelectorOptions
title: UseSelectorOptions
---
-Defined in: [packages/react-store/src/useSelector.ts:4](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSelector.ts#L4)
+Defined in: [packages/react-store/src/useSelector.ts:3](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSelector.ts#L3)
## Type Parameters
@@ -19,7 +19,7 @@ Defined in: [packages/react-store/src/useSelector.ts:4](https://github.com/TanSt
optional compare?: (a, b) => boolean;
```
-Defined in: [packages/react-store/src/useSelector.ts:5](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSelector.ts#L5)
+Defined in: [packages/react-store/src/useSelector.ts:4](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSelector.ts#L4)
#### Parameters
diff --git a/docs/installation.md b/docs/installation.md
index bdbc29af..035f9654 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -11,7 +11,7 @@ You can install TanStack Store with any [NPM](https://npmjs.com) package manager
npm install @tanstack/react-store
```
-TanStack Store is compatible with React v16.8+ and is currently only compatible with ReactDOM only. If you would like to contribute to the React Native adapter, please reach out to us on [Discord](https://tlinz.com/discord).
+TanStack Store is compatible with React v18+.
## Preact
@@ -27,7 +27,7 @@ TanStack Store is compatible with Preact 10+.
npm install @tanstack/vue-store
```
-TanStack Store is compatible with Vue 2 and 3.
+TanStack Store is compatible with Vue 3.
## Angular
diff --git a/examples/react/atoms/package.json b/examples/react/atoms/package.json
index fdd138e1..bafccee9 100644
--- a/examples/react/atoms/package.json
+++ b/examples/react/atoms/package.json
@@ -9,7 +9,7 @@
"test:types": "tsc"
},
"dependencies": {
- "@tanstack/react-store": "^0.11.2",
+ "@tanstack/react-store": "^1.0.0-alpha.0",
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
diff --git a/examples/react/simple/package.json b/examples/react/simple/package.json
index bcab4cc2..08a04ab4 100644
--- a/examples/react/simple/package.json
+++ b/examples/react/simple/package.json
@@ -9,7 +9,7 @@
"test:types": "tsc"
},
"dependencies": {
- "@tanstack/react-store": "^0.11.2",
+ "@tanstack/react-store": "^1.0.0-alpha.0",
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
diff --git a/examples/react/store-actions/package.json b/examples/react/store-actions/package.json
index 5a61ccee..38c3a3f5 100644
--- a/examples/react/store-actions/package.json
+++ b/examples/react/store-actions/package.json
@@ -9,7 +9,7 @@
"test:types": "tsc"
},
"dependencies": {
- "@tanstack/react-store": "^0.11.2",
+ "@tanstack/react-store": "^1.0.0-alpha.0",
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
diff --git a/examples/react/store-context/package.json b/examples/react/store-context/package.json
index 46ef8457..892012e5 100644
--- a/examples/react/store-context/package.json
+++ b/examples/react/store-context/package.json
@@ -9,7 +9,7 @@
"test:types": "tsc"
},
"dependencies": {
- "@tanstack/react-store": "^0.11.2",
+ "@tanstack/react-store": "^1.0.0-alpha.0",
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
diff --git a/examples/react/stores/package.json b/examples/react/stores/package.json
index 97d9526e..a677fd1c 100644
--- a/examples/react/stores/package.json
+++ b/examples/react/stores/package.json
@@ -9,7 +9,7 @@
"test:types": "tsc"
},
"dependencies": {
- "@tanstack/react-store": "^0.11.2",
+ "@tanstack/react-store": "^1.0.0-alpha.0",
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
diff --git a/examples/vue/atoms/package.json b/examples/vue/atoms/package.json
index fe066dc2..93676b80 100644
--- a/examples/vue/atoms/package.json
+++ b/examples/vue/atoms/package.json
@@ -10,7 +10,7 @@
"serve": "vite preview"
},
"dependencies": {
- "@tanstack/vue-store": "^0.11.2",
+ "@tanstack/vue-store": "^1.0.0-alpha.0",
"vue": "^3.5.32"
},
"devDependencies": {
diff --git a/examples/vue/simple/package.json b/examples/vue/simple/package.json
index 3954b448..8c5a1dde 100644
--- a/examples/vue/simple/package.json
+++ b/examples/vue/simple/package.json
@@ -10,7 +10,7 @@
"serve": "vite preview"
},
"dependencies": {
- "@tanstack/vue-store": "^0.11.2",
+ "@tanstack/vue-store": "^1.0.0-alpha.0",
"vue": "^3.5.32"
},
"devDependencies": {
diff --git a/examples/vue/store-actions/package.json b/examples/vue/store-actions/package.json
index 87ef13bf..442e2abc 100644
--- a/examples/vue/store-actions/package.json
+++ b/examples/vue/store-actions/package.json
@@ -10,7 +10,7 @@
"serve": "vite preview"
},
"dependencies": {
- "@tanstack/vue-store": "^0.11.2",
+ "@tanstack/vue-store": "^1.0.0-alpha.0",
"vue": "^3.5.32"
},
"devDependencies": {
diff --git a/examples/vue/store-context/package.json b/examples/vue/store-context/package.json
index e37980da..ea8d0605 100644
--- a/examples/vue/store-context/package.json
+++ b/examples/vue/store-context/package.json
@@ -10,7 +10,7 @@
"serve": "vite preview"
},
"dependencies": {
- "@tanstack/vue-store": "^0.11.2",
+ "@tanstack/vue-store": "^1.0.0-alpha.0",
"vue": "^3.5.32"
},
"devDependencies": {
diff --git a/examples/vue/stores/package.json b/examples/vue/stores/package.json
index f4ea2940..bae00b86 100644
--- a/examples/vue/stores/package.json
+++ b/examples/vue/stores/package.json
@@ -10,7 +10,7 @@
"serve": "vite preview"
},
"dependencies": {
- "@tanstack/vue-store": "^0.11.2",
+ "@tanstack/vue-store": "^1.0.0-alpha.0",
"vue": "^3.5.32"
},
"devDependencies": {
diff --git a/knip.json b/knip.json
index 1be79759..fe7b1246 100644
--- a/knip.json
+++ b/knip.json
@@ -1,11 +1,11 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"workspaces": {
+ "packages/react-store": {
+ "entry": ["tests/fixtures/selector-retention.mts"]
+ },
"packages/store": {
"entry": ["src/signal.ts"]
- },
- "packages/vue-store": {
- "ignoreDependencies": ["vue2", "vue2.7"]
}
},
"ignoreWorkspaces": ["examples/**"]
diff --git a/packages/react-store/CHANGELOG.md b/packages/react-store/CHANGELOG.md
index ca9e6f18..4d6afd14 100644
--- a/packages/react-store/CHANGELOG.md
+++ b/packages/react-store/CHANGELOG.md
@@ -1,5 +1,13 @@
# @tanstack/react-store
+## 1.0.0-alpha.0
+
+### Major Changes
+
+- [#362](https://github.com/TanStack/store/pull/362) [`5567af4`](https://github.com/TanStack/store/commit/5567af42a4eee4b5befad3acd2f589d3f4deb5bd) - `@tanstack/react-store` now requires React 18 or newer (`peerDependencies` are `react` and `react-dom` `^18.0.0 || ^19.0.0`); support for React 16.8 and 17 has been dropped.
+
+ `useSelector` builds on React's built-in `useSyncExternalStore` with a single memoized selection ref instead of the `use-sync-external-store/shim/with-selector` helper: fewer hook slots and allocations per subscribed component, no per-component passive effect, and the `use-sync-external-store` dependency is gone from consumer bundles. The public API and selection semantics of `useSelector`, `useAtom`, `_useStore` and `useStore` are unchanged.
+
## 0.11.2
### Patch Changes
diff --git a/packages/react-store/package.json b/packages/react-store/package.json
index a93c2c77..ccadd002 100644
--- a/packages/react-store/package.json
+++ b/packages/react-store/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-store",
- "version": "0.11.2",
+ "version": "1.0.0-alpha.0",
"description": "Framework agnostic type-safe store w/ reactive framework adapters",
"author": "Tanner Linsley",
"license": "MIT",
@@ -29,6 +29,7 @@
"test:types:ts59": "tsc",
"test:lib": "vitest",
"test:lib:dev": "pnpm run test:lib --watch",
+ "test:perf": "NODE_ENV=production vitest bench --run --coverage.enabled=false --typecheck.enabled=false",
"test:build": "publint --strict",
"build": "tsdown --tsconfig tsconfig.build.json"
},
@@ -49,20 +50,18 @@
"src"
],
"dependencies": {
- "@tanstack/store": "workspace:*",
- "use-sync-external-store": "^1.6.0"
+ "@tanstack/store": "workspace:*"
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
- "@types/use-sync-external-store": "^1.5.0",
"@vitejs/plugin-react": "^6.0.1",
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
"peerDependencies": {
- "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"
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
}
}
diff --git a/packages/react-store/src/useSelector.ts b/packages/react-store/src/useSelector.ts
index 0f2593f0..e736ffbe 100644
--- a/packages/react-store/src/useSelector.ts
+++ b/packages/react-store/src/useSelector.ts
@@ -1,14 +1,9 @@
-import { useCallback } from 'react'
-import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
+import { useRef, useSyncExternalStore } from 'react'
export interface UseSelectorOptions {
compare?: (a: TSelected, b: TSelected) => boolean
}
-type SyncExternalStoreSubscribe = Parameters<
- typeof useSyncExternalStoreWithSelector
->[0]
-
type SelectionSource = {
get: () => T
subscribe: (listener: (value: T) => void) => {
@@ -16,10 +11,66 @@ type SelectionSource = {
}
}
+/**
+ * Per-component state, mutated in place. The inputs and the callbacks built
+ * for them are written during render; the selection is written by whichever
+ * `getSnapshot` closure computed it last and is keyed on that closure.
+ */
+type Instance = {
+ source?: SelectionSource
+ selector?: (snapshot: TSource) => TSelected
+ compare?: (a: TSelected, b: TSelected) => boolean
+ subscribe?: (onStoreChange: () => void) => () => void
+ getSnapshot?: () => TSelected
+ // Private cache keys stay short because property names survive minification.
+ o: (() => TSelected) | null // selection owner
+ s?: TSource // snapshot
+ v?: TSelected // selected value
+}
+
+function identity(snapshot: TSource): TSelected {
+ return snapshot as unknown as TSelected
+}
+
function defaultCompare(a: T, b: T) {
return a === b
}
+// Keep snapshot inputs out of the subscription's closure scope. This also
+// avoids nesting the snapshot callback inside a render's conditional scope.
+function createGetSnapshot(
+ source: SelectionSource,
+ selector: (snapshot: TSource) => TSelected,
+ compare: (a: TSelected, b: TSelected) => boolean,
+ instance: Instance,
+): () => TSelected {
+ const getSnapshot = () => {
+ const snapshot = source.get()
+
+ if (instance.o !== getSnapshot || instance.s !== snapshot) {
+ const selected = selector(snapshot)
+
+ // Keep the previous selection's identity when `compare` considers the
+ // new one equal so that `useSyncExternalStore` does not re-render the
+ // component. Like the former `use-sync-external-store/shim/with-selector`
+ // helper, this compares against the previous selection even when the
+ // selector identity changed: inline selectors are recreated on every
+ // render and must still return the same object when the selection is
+ // equal.
+ if (instance.o === null || !compare(instance.v as TSelected, selected)) {
+ instance.v = selected
+ }
+
+ instance.o = getSnapshot
+ instance.s = snapshot
+ }
+
+ return instance.v as TSelected
+ }
+
+ return getSnapshot
+}
+
/**
* Selects a slice of state from an atom or store and subscribes the component
* to that selection.
@@ -42,26 +93,54 @@ function defaultCompare(a: T, b: T) {
*/
export function useSelector>(
source: SelectionSource,
- selector: (snapshot: TSource) => TSelected = (s) => s as unknown as TSelected,
+ selector: (snapshot: TSource) => TSelected = identity,
options?: UseSelectorOptions,
): TSelected {
const compare = options?.compare ?? defaultCompare
- const subscribe: SyncExternalStoreSubscribe = useCallback(
- (handleStoreChange) => {
- const { unsubscribe } = source.subscribe(handleStoreChange)
- return unsubscribe
- },
- [source],
- )
+ // One ref instead of `useCallback`s. `useSyncExternalStore` re-subscribes
+ // whenever `subscribe` changes identity and schedules a passive effect plus
+ // a consistency check whenever `getSnapshot` does, so both are only rebuilt
+ // when their inputs change. With a stable selector, a re-render that leaves
+ // the store untouched costs no allocations and no effects.
+ const instanceRef = useRef | null>(null)
+ const instance = instanceRef.current ?? (instanceRef.current = { o: null })
+ const sourceChanged = instance.source !== source
- const getSnapshot = useCallback(() => source.get(), [source])
+ if (sourceChanged) {
+ instance.subscribe = (onStoreChange) => {
+ const subscription = source.subscribe(onStoreChange)
+
+ // Call `unsubscribe` on the subscription so sources that rely on `this`
+ // keep working.
+ return () => subscription.unsubscribe()
+ }
+ }
+
+ if (
+ sourceChanged ||
+ instance.selector !== selector ||
+ instance.compare !== compare
+ ) {
+ instance.source = source
+ instance.selector = selector
+ instance.compare = compare
+
+ // The closure captures its inputs instead of reading them from the
+ // instance so that a render which suspends with a different selector
+ // cannot change what the committed subscription selects. The selection is
+ // keyed on the closure for the same reason.
+ instance.getSnapshot = createGetSnapshot(
+ source,
+ selector,
+ compare,
+ instance,
+ )
+ }
- return useSyncExternalStoreWithSelector(
- subscribe,
- getSnapshot,
- getSnapshot,
- selector,
- compare,
+ return useSyncExternalStore(
+ instance.subscribe!,
+ instance.getSnapshot!,
+ instance.getSnapshot,
)
}
diff --git a/packages/react-store/tests/fixtures/selector-retention.mts b/packages/react-store/tests/fixtures/selector-retention.mts
new file mode 100644
index 00000000..ecc06f51
--- /dev/null
+++ b/packages/react-store/tests/fixtures/selector-retention.mts
@@ -0,0 +1,64 @@
+import assert from 'node:assert/strict'
+import { setImmediate as endTurn } from 'node:timers/promises'
+// jsdom is a runtime-only test dependency in this workspace.
+// @ts-expect-error No declaration package is installed for jsdom.
+import { JSDOM } from 'jsdom'
+import { createElement, useLayoutEffect } from 'react'
+import { createAtom } from '@tanstack/store'
+const { useSelector } = (await import(
+ new URL('../../src/useSelector.ts', import.meta.url).href
+)) as typeof import('../../src/useSelector')
+
+const dom = new JSDOM('')
+globalThis.window = dom.window
+globalThis.document = dom.window.document
+Object.defineProperty(globalThis, 'navigator', {
+ value: dom.window.navigator,
+ configurable: true,
+})
+const { createRoot } = await import('react-dom/client')
+const { flushSync } = await import('react-dom')
+type Payload = { payload?: Array; update?: number }
+const source = createAtom<{ item: Payload }>({ item: {} })
+const previous = new WeakRef({
+ payload: new Array(100_000).fill('previous'),
+})
+source.set({ item: previous.deref()! })
+const container = document.getElementById('root')
+assert.ok(container)
+const root = createRoot(container)
+let observed: WeakRef | undefined
+
+function Component({ field }: { field: 'item' }) {
+ const selected = useSelector(source, (state) => state[field])
+ useLayoutEffect(() => {
+ observed = new WeakRef(selected)
+ }, [selected])
+ return null
+}
+
+async function collect() {
+ for (let attempt = 0; attempt < 5; attempt++) {
+ await endTurn()
+ globalThis.gc!()
+ }
+}
+
+try {
+ flushSync(() => root.render(createElement(Component, { field: 'item' })))
+ assert.equal(observed!.deref(), previous.deref())
+ await endTurn()
+ for (let update = 0; update < 100; update++) {
+ flushSync(() => source.set({ item: { update } }))
+ }
+ await collect()
+ assert.equal(observed!.deref()!.update, 99)
+ assert.equal(
+ previous.deref() === undefined,
+ true,
+ 'The subscription must release the first render selection while mounted',
+ )
+} finally {
+ root.unmount()
+ dom.window.close()
+}
diff --git a/packages/react-store/tests/index.test.tsx b/packages/react-store/tests/index.test.tsx
index 5f36579b..64c46d71 100644
--- a/packages/react-store/tests/index.test.tsx
+++ b/packages/react-store/tests/index.test.tsx
@@ -1,4 +1,7 @@
-import { Suspense, startTransition, use, useState } from 'react'
+import { execFile } from 'node:child_process'
+import { resolve } from 'node:path'
+import { promisify } from 'node:util'
+import { StrictMode, Suspense, startTransition, use, useState } from 'react'
import {
act,
fireEvent,
@@ -637,6 +640,460 @@ describe('store hooks', () => {
})
})
+describe('useSelector selection memo', () => {
+ type State = { a: number; b: number }
+
+ it.each<[unknown, unknown]>([
+ [undefined, false],
+ [null, 0],
+ [false, null],
+ [0, undefined],
+ ])('caches a %s snapshot with a %s selection', (snapshot, selected) => {
+ const atom = createAtom(snapshot)
+ const selector = vi.fn((_snapshot: unknown) => selected)
+ const compare = vi.fn(Object.is)
+ const { result, rerender } = renderHook(() =>
+ useSelector(atom, selector, { compare }),
+ )
+
+ expect(result.current).toBe(selected)
+ expect(selector).toHaveBeenCalledTimes(1)
+ expect(selector).toHaveBeenCalledWith(snapshot)
+ expect(compare).not.toHaveBeenCalled()
+
+ rerender()
+
+ expect(result.current).toBe(selected)
+ expect(selector).toHaveBeenCalledTimes(1)
+ expect(compare).not.toHaveBeenCalled()
+ })
+
+ it('compares a cached undefined selection when the selector or comparator changes', () => {
+ type Selected = boolean | undefined
+ type Props = {
+ selector: (snapshot: number) => Selected
+ compare: (a: Selected, b: Selected) => boolean
+ }
+ const atom = createAtom(0)
+ const selectUndefined = vi.fn((_snapshot: number): Selected => undefined)
+ const selectFalse = vi.fn((_snapshot: number): Selected => false)
+ const equal = vi.fn((_a: Selected, _b: Selected) => true)
+ const strict = vi.fn((a: Selected, b: Selected) => a === b)
+ const { result, rerender } = renderHook(
+ ({ selector, compare }: Props) =>
+ useSelector(atom, selector, { compare }),
+ { initialProps: { selector: selectUndefined, compare: equal } },
+ )
+
+ expect(result.current).toBeUndefined()
+ expect(equal).not.toHaveBeenCalled()
+
+ rerender({ selector: selectFalse, compare: equal })
+
+ expect(selectFalse).toHaveBeenCalledWith(0)
+ expect(equal).toHaveBeenCalledWith(undefined, false)
+ expect(result.current).toBeUndefined()
+
+ rerender({ selector: selectFalse, compare: strict })
+
+ expect(strict).toHaveBeenCalledWith(undefined, false)
+ expect(result.current).toBe(false)
+ })
+
+ it('does not re-run a stable selector when the component re-renders with an unchanged store value', () => {
+ const store = createStore({ a: 1, b: 2 })
+ const selector = vi.fn((state: State) => state.a)
+
+ function Comp({ label }: { label: string }) {
+ const value = useSelector(store, selector)
+
+ return (
+
+ {label}: {value}
+
+ )
+ }
+
+ const { getByText, rerender } = render()
+
+ expect(getByText('First: 1')).toBeInTheDocument()
+ expect(selector).toHaveBeenCalledTimes(1)
+
+ rerender()
+
+ expect(getByText('Second: 1')).toBeInTheDocument()
+ expect(selector).toHaveBeenCalledTimes(1)
+ })
+
+ it('keeps the previous selection identity when compare returns true', () => {
+ const store = createStore({ items: [1, 2], other: 0 })
+ const selections: Array<{ items: Array }> = []
+
+ function Comp() {
+ const selected = useSelector(store, (state) => ({ items: state.items }), {
+ compare: shallow,
+ })
+ selections.push(selected)
+
+ return Items: {selected.items.join(',')}
+ }
+
+ const { getByText, rerender } = render()
+
+ expect(getByText('Items: 1,2')).toBeInTheDocument()
+ expect(selections).toHaveLength(1)
+
+ // The selection is shallowly equal, so the component must not re-render.
+ act(() => {
+ store.setState((prev) => ({ ...prev, other: 1 }))
+ })
+
+ expect(selections).toHaveLength(1)
+
+ // The inline selector has a new identity on every render; compare still
+ // runs against the previous selection so the component receives the same
+ // object.
+ rerender()
+
+ expect(selections).toHaveLength(2)
+ expect(selections[1]).toBe(selections[0])
+
+ act(() => {
+ store.setState((prev) => ({ ...prev, items: [...prev.items, 3] }))
+ })
+
+ expect(getByText('Items: 1,2,3')).toBeInTheDocument()
+ expect(selections).toHaveLength(3)
+ expect(selections[2]).not.toBe(selections[0])
+ })
+
+ it('re-runs a new selector and returns its selection', () => {
+ const store = createStore({ a: 1, b: 2 })
+ const selectA = vi.fn((state: State) => state.a)
+ const selectB = vi.fn((state: State) => state.b)
+
+ function Comp({ selector }: { selector: (state: State) => number }) {
+ const value = useSelector(store, selector)
+
+ return Value: {value}
+ }
+
+ const { getByText, rerender } = render()
+
+ expect(getByText('Value: 1')).toBeInTheDocument()
+ expect(selectA).toHaveBeenCalledTimes(1)
+ expect(selectB).not.toHaveBeenCalled()
+
+ rerender()
+
+ expect(getByText('Value: 2')).toBeInTheDocument()
+ expect(selectA).toHaveBeenCalledTimes(1)
+ expect(selectB).toHaveBeenCalledTimes(1)
+
+ rerender()
+
+ expect(getByText('Value: 1')).toBeInTheDocument()
+ expect(selectA).toHaveBeenCalledTimes(2)
+ expect(selectB).toHaveBeenCalledTimes(1)
+ })
+
+ it('re-runs the installed selector exactly once per store update', () => {
+ const store = createStore({ a: 1, b: 2 })
+ const selector = vi.fn((state: State) => state.a)
+ const renderSpy = vi.fn()
+
+ function Comp() {
+ const value = useSelector(store, selector)
+ renderSpy()
+
+ return Value: {value}
+ }
+
+ const { getByText } = render()
+
+ expect(getByText('Value: 1')).toBeInTheDocument()
+ expect(selector).toHaveBeenCalledTimes(1)
+ expect(renderSpy).toHaveBeenCalledTimes(1)
+
+ act(() => {
+ store.setState((prev) => ({ ...prev, a: 10 }))
+ })
+
+ expect(getByText('Value: 10')).toBeInTheDocument()
+ expect(selector).toHaveBeenCalledTimes(2)
+ expect(renderSpy).toHaveBeenCalledTimes(2)
+
+ // An update that leaves the selection unchanged still runs the selector once
+ // to find that out, but does not re-render.
+ act(() => {
+ store.setState((prev) => ({ ...prev, b: 20 }))
+ })
+
+ expect(getByText('Value: 10')).toBeInTheDocument()
+ expect(selector).toHaveBeenCalledTimes(3)
+ expect(renderSpy).toHaveBeenCalledTimes(2)
+ })
+})
+
+describe('useSelector subscription cleanup', () => {
+ it('releases the first render selection while the component stays mounted', async () => {
+ await promisify(execFile)(
+ process.execPath,
+ ['--expose-gc', resolve('tests/fixtures/selector-retention.mts')],
+ { env: { ...process.env, NODE_ENV: 'production' } },
+ )
+ })
+
+ it('keeps the subscription through StrictMode replay and inline selector changes', () => {
+ class Source {
+ value = 1
+ listeners = new Set<(value: number) => void>()
+ subscriptions = 0
+ cleanups = 0
+
+ get() {
+ return this.value
+ }
+
+ subscribe(listener: (value: number) => void) {
+ this.subscriptions++
+ this.listeners.add(listener)
+ return {
+ unsubscribe: () => {
+ this.cleanups++
+ this.listeners.delete(listener)
+ },
+ }
+ }
+ }
+
+ const source = new Source()
+ function Comp({ offset }: { offset: number }) {
+ const value = useSelector(source, (state) => state + offset, {
+ compare: (a, b) => a === b,
+ })
+ return Value: {value}
+ }
+
+ const { getByText, rerender, unmount } = render(
+
+
+ ,
+ )
+ expect(getByText('Value: 1')).toBeInTheDocument()
+ expect(source.listeners.size).toBe(1)
+ expect(source.subscriptions).toBe(source.cleanups + 1)
+ const subscriptions = source.subscriptions
+ const cleanups = source.cleanups
+
+ rerender(
+
+
+ ,
+ )
+ expect(getByText('Value: 11')).toBeInTheDocument()
+ expect(source.subscriptions).toBe(subscriptions)
+ expect(source.cleanups).toBe(cleanups)
+
+ unmount()
+ expect(source.listeners.size).toBe(0)
+ expect(source.cleanups).toBe(source.subscriptions)
+ })
+
+ it('keeps the committed source subscribed while a source change suspends', async () => {
+ const first = createAtom(1)
+ const second = createAtom(10)
+ const subscribeFirst = vi.spyOn(first, 'subscribe')
+ const subscribeSecond = vi.spyOn(second, 'subscribe')
+ const pending = new Promise(() => {})
+ const readSecond = vi.fn((value: number) => value)
+
+ function Value({ mode }: { mode: 'first' | 'second' }) {
+ const value = useSelector(
+ mode === 'first' ? first : second,
+ mode === 'first' ? undefined : readSecond,
+ )
+ if (mode === 'second') {
+ use(pending)
+ }
+ return
+ }
+
+ function Comp() {
+ const [mode, setMode] = useState<'first' | 'second'>('first')
+ return (
+ <>
+
+ Loading
}>
+
+
+ >
+ )
+ }
+
+ const { getByRole, getByTestId } = render()
+ expect(subscribeFirst).toHaveBeenCalledTimes(1)
+ fireEvent.click(getByRole('button', { name: 'Switch source' }))
+ await waitFor(() => expect(readSecond).toHaveBeenCalled())
+ expect(subscribeSecond).not.toHaveBeenCalled()
+
+ act(() => {
+ first.set(2)
+ second.set(20)
+ })
+ expect(getByTestId('source-value')).toHaveTextContent('2')
+
+ expect(subscribeSecond).not.toHaveBeenCalled()
+ })
+
+ it('unsubscribes through the subscription object so `this`-based sources clean up', () => {
+ const listeners = new Set<(value: number) => void>()
+
+ class Subscription {
+ closed = false
+
+ constructor(private readonly listener: (value: number) => void) {}
+
+ // Throws if React calls it detached from the subscription object.
+ unsubscribe() {
+ this.closed = true
+ listeners.delete(this.listener)
+ }
+ }
+
+ const source = {
+ get: () => 1,
+ subscribe: (listener: (value: number) => void) => {
+ listeners.add(listener)
+ return new Subscription(listener)
+ },
+ }
+
+ function Comp() {
+ const value = useSelector(source)
+
+ return Value: {value}
+ }
+
+ const { getByText, unmount } = render()
+
+ expect(getByText('Value: 1')).toBeInTheDocument()
+ expect(listeners.size).toBe(1)
+
+ unmount()
+
+ expect(listeners.size).toBe(0)
+ })
+
+ it('moves the subscription when the source changes', () => {
+ function createSource(initial: number) {
+ const listeners = new Set<(value: number) => void>()
+ let value = initial
+
+ return {
+ listeners,
+ get: () => value,
+ set: (next: number) => {
+ value = next
+ listeners.forEach((listener) => listener(next))
+ },
+ subscribe: (listener: (value: number) => void) => {
+ listeners.add(listener)
+ return {
+ unsubscribe: () => {
+ listeners.delete(listener)
+ },
+ }
+ },
+ }
+ }
+
+ const first = createSource(1)
+ const second = createSource(10)
+
+ function Comp({ source }: { source: typeof first }) {
+ const value = useSelector(source)
+
+ return Value: {value}
+ }
+
+ const { getByText, rerender } = render()
+
+ expect(getByText('Value: 1')).toBeInTheDocument()
+ expect(first.listeners.size).toBe(1)
+
+ rerender()
+
+ expect(getByText('Value: 10')).toBeInTheDocument()
+ expect(first.listeners.size).toBe(0)
+ expect(second.listeners.size).toBe(1)
+
+ act(() => {
+ second.set(20)
+ })
+
+ expect(getByText('Value: 20')).toBeInTheDocument()
+ })
+})
+
+describe('useSelector compare changes', () => {
+ it('uses the compare function from the latest render', () => {
+ type State = { a: number; b: number }
+ const store = createStore({ a: 0, b: 0 })
+ const compareA = (x: State, y: State) => x.a === y.a
+ const compareB = (x: State, y: State) => x.b === y.b
+ const renderSpy = vi.fn()
+
+ function Comp({ compare }: { compare: typeof compareA }) {
+ const value = useSelector(store, undefined, { compare })
+ renderSpy()
+
+ return (
+
+ a{value.a} b{value.b}
+
+ )
+ }
+
+ const { getByText, rerender } = render()
+
+ expect(getByText('a0 b0')).toBeInTheDocument()
+
+ // compareA ignores `b`, so this update does not re-render.
+ act(() => {
+ store.setState((prev) => ({ ...prev, b: 1 }))
+ })
+
+ expect(getByText('a0 b0')).toBeInTheDocument()
+ expect(renderSpy).toHaveBeenCalledTimes(1)
+
+ rerender()
+
+ expect(getByText('a0 b1')).toBeInTheDocument()
+ expect(renderSpy).toHaveBeenCalledTimes(2)
+
+ // compareB ignores `a`, so this update does not re-render.
+ act(() => {
+ store.setState((prev) => ({ ...prev, a: 1 }))
+ })
+
+ expect(getByText('a0 b1')).toBeInTheDocument()
+ expect(renderSpy).toHaveBeenCalledTimes(2)
+
+ act(() => {
+ store.setState((prev) => ({ ...prev, b: 2 }))
+ })
+
+ expect(getByText('a1 b2')).toBeInTheDocument()
+ expect(renderSpy).toHaveBeenCalledTimes(3)
+ })
+})
+
describe('useStore', () => {
it('is a compatibility alias for useSelector', async () => {
const store = createStore(0)
diff --git a/packages/react-store/tests/useSelector.bench.tsx b/packages/react-store/tests/useSelector.bench.tsx
new file mode 100644
index 00000000..a97b3bd1
--- /dev/null
+++ b/packages/react-store/tests/useSelector.bench.tsx
@@ -0,0 +1,155 @@
+/* eslint-disable @eslint-react/dom-no-flush-sync -- Time committed production updates synchronously. */
+import { createElement, useLayoutEffect } from 'react'
+import { createRoot } from 'react-dom/client'
+import { flushSync } from 'react-dom'
+import { afterAll, bench, describe, expect } from 'vitest'
+import { createAtom } from '@tanstack/store'
+import { useSelector } from '../src/index'
+
+type State = { value: number; ignored: number }
+type Mode = 'stable' | 'inline' | 'equal'
+const selectValue = (state: State) => state.value
+const compareValue = (a: { value: number }, b: { value: number }) =>
+ a.value === b.value
+
+function createSource(value: number) {
+ const atom = createAtom({ value, ignored: 0 })
+ let active = 0
+ return {
+ source: {
+ get: () => atom.get(),
+ subscribe(listener: (value: State) => void) {
+ const subscription = atom.subscribe(listener)
+ active++
+ return {
+ unsubscribe() {
+ subscription.unsubscribe()
+ active--
+ },
+ }
+ },
+ },
+ setValue: (next: number) =>
+ atom.set((state) => ({ ...state, value: next })),
+ setIgnored: (next: number) =>
+ atom.set((state) => ({ ...state, ignored: next })),
+ active: () => active,
+ }
+}
+
+function mount(mode: Mode) {
+ const first = createSource(0)
+ const second = createSource(10)
+ const container = document.createElement('div')
+ const root = createRoot(container)
+ let observed = 0
+ let renders = 0
+
+ function Component({
+ source,
+ offset,
+ }: {
+ source: typeof first.source
+ offset: number
+ }) {
+ const selected = useSelector(
+ source,
+ mode === 'stable'
+ ? selectValue
+ : mode === 'equal'
+ ? (state) => ({ value: state.value + offset })
+ : (state) => state.value + offset,
+ mode === 'equal'
+ ? {
+ compare: compareValue as (
+ a: number | { value: number },
+ b: number | { value: number },
+ ) => boolean,
+ }
+ : undefined,
+ )
+ useLayoutEffect(() => {
+ observed = typeof selected === 'number' ? selected : selected.value
+ renders++
+ })
+ return null
+ }
+
+ function render(offset = 0, source = first.source) {
+ flushSync(() =>
+ root.render(
+ Array.from({ length: 100 }, (_, key) =>
+ createElement(Component, { key, offset, source }),
+ ),
+ ),
+ )
+ }
+ render()
+ return {
+ update: (value: number) => flushSync(() => first.setValue(value)),
+ ignored: (value: number) => flushSync(() => first.setIgnored(value)),
+ render,
+ switchSource: (value: number) =>
+ render(0, value % 2 ? second.source : first.source),
+ read: () => observed,
+ renders: () => renders,
+ active: () => [first.active(), second.active()],
+ unmount: () => root.unmount(),
+ }
+}
+
+// Validate each workload through rendered values and public subscriptions before
+// timing it. An equality fast path must suppress rendering, not skip updates.
+for (const mode of ['stable', 'inline', 'equal'] as const) {
+ const app = mount(mode)
+ expect(app.active()).toEqual([100, 0])
+ app.update(1)
+ expect(app.read()).toBe(1)
+ const renders = app.renders()
+ app.ignored(1)
+ expect(app.renders()).toBe(renders)
+ app.render(5)
+ expect(app.read()).toBe(mode === 'stable' ? 1 : 6)
+ app.switchSource(1)
+ expect(app.read()).toBe(10)
+ expect(app.active()).toEqual([0, 100])
+ app.unmount()
+ expect(app.active()).toEqual([0, 0])
+}
+
+describe('useSelector with 100 subscribers', () => {
+ const apps: Array> = []
+ afterAll(() => apps.forEach((app) => app.unmount()))
+
+ for (const [name, mode, operation] of [
+ ['stable selector updates', 'stable', 'update'],
+ ['inline selector updates', 'inline', 'update'],
+ ['equal allocating projections', 'equal', 'ignored'],
+ ['stable selector parent renders', 'stable', 'render'],
+ ['captured prop parent renders', 'inline', 'render'],
+ ['source changes and cleanup', 'inline', 'switchSource'],
+ ] as const) {
+ const app = mount(mode)
+ apps.push(app)
+ let value = 0
+ bench(
+ name,
+ () => {
+ for (let iteration = 0; iteration < 50; iteration++) {
+ app[operation](++value)
+ }
+ },
+ { time: 1000, warmupTime: 500, iterations: 100 },
+ )
+ }
+
+ bench(
+ 'mount and unmount inline selectors',
+ () => {
+ for (let iteration = 0; iteration < 10; iteration++) {
+ mount('inline').unmount()
+ }
+ },
+ { time: 1000, warmupTime: 500, iterations: 100 },
+ )
+})
diff --git a/packages/vue-store/CHANGELOG.md b/packages/vue-store/CHANGELOG.md
index 592fcc77..30cedd20 100644
--- a/packages/vue-store/CHANGELOG.md
+++ b/packages/vue-store/CHANGELOG.md
@@ -1,5 +1,13 @@
# @tanstack/vue-store
+## 1.0.0-alpha.0
+
+### Major Changes
+
+- [#389](https://github.com/TanStack/store/pull/389) [`c9bef08`](https://github.com/TanStack/store/commit/c9bef08b7b5e5e458e41622cf030463361b2dac9) - Drop Vue 2 support from `@tanstack/vue-store`. The adapter now requires Vue 3 (`vue: ^3.0.0`) and imports Vue APIs and types directly, removing its `vue-demi` dependency and optional `@vue/composition-api` peer dependency.
+
+ Vue 2 applications must upgrade to Vue 3 or keep using a previous version of `@tanstack/vue-store`. The adapter's public hooks and their behavior are unchanged for Vue 3 applications.
+
## 0.11.2
### Patch Changes
diff --git a/packages/vue-store/package.json b/packages/vue-store/package.json
index b6517925..12aaed04 100644
--- a/packages/vue-store/package.json
+++ b/packages/vue-store/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/vue-store",
- "version": "0.11.2",
+ "version": "1.0.0-alpha.0",
"description": "Framework agnostic type-safe store w/ reactive framework adapters",
"author": "Tanner Linsley",
"license": "MIT",
@@ -27,11 +27,7 @@
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
"test:types:ts59": "tsc",
- "test:lib": "pnpm run test:lib:3",
- "test:lib:2": "vue-demi-switch 2 vue2 && vitest",
- "test:lib:2.7": "vue-demi-switch 2.7 vue2.7 && vitest",
- "test:lib:3": "vue-demi-switch 3 && vitest",
- "test:lib:fixme": "pnpm run test:lib:2 && pnpm run test:lib:2.7 && pnpm run test:lib:3",
+ "test:lib": "vitest",
"test:lib:dev": "pnpm run test:lib --watch",
"test:build": "publint --strict",
"build": "tsdown --tsconfig tsconfig.build.json"
@@ -53,24 +49,14 @@
"src"
],
"dependencies": {
- "@tanstack/store": "workspace:*",
- "vue-demi": "^0.14.10"
+ "@tanstack/store": "workspace:*"
},
"devDependencies": {
"@testing-library/vue": "^8.1.0",
"@vitejs/plugin-vue": "^6.0.5",
- "@vue/composition-api": "^1.7.2",
- "vue": "^3.5.43",
- "vue2": "npm:vue@2.6",
- "vue2.7": "npm:vue@2.7"
+ "vue": "^3.5.43"
},
"peerDependencies": {
- "@vue/composition-api": "^1.2.1",
- "vue": "^2.5.0 || ^3.0.0"
- },
- "peerDependenciesMeta": {
- "@vue/composition-api": {
- "optional": true
- }
+ "vue": "^3.0.0"
}
}
diff --git a/packages/vue-store/src/_useStore.ts b/packages/vue-store/src/_useStore.ts
index f7401fb8..8e990f7c 100644
--- a/packages/vue-store/src/_useStore.ts
+++ b/packages/vue-store/src/_useStore.ts
@@ -1,5 +1,5 @@
import { useSelector } from './useSelector'
-import type { Ref } from 'vue-demi'
+import type { Ref } from 'vue'
import type { Store, StoreActionMap } from '@tanstack/store'
import type { UseSelectorOptions } from './useSelector'
diff --git a/packages/vue-store/src/useAtom.ts b/packages/vue-store/src/useAtom.ts
index 44680038..f5271d64 100644
--- a/packages/vue-store/src/useAtom.ts
+++ b/packages/vue-store/src/useAtom.ts
@@ -1,5 +1,5 @@
import { useSelector } from './useSelector'
-import type { Ref } from 'vue-demi'
+import type { Ref } from 'vue'
import type { Atom } from '@tanstack/store'
import type { UseSelectorOptions } from './useSelector'
diff --git a/packages/vue-store/src/useSelector.ts b/packages/vue-store/src/useSelector.ts
index 1a3cea66..1ed054e4 100644
--- a/packages/vue-store/src/useSelector.ts
+++ b/packages/vue-store/src/useSelector.ts
@@ -1,5 +1,5 @@
-import { onScopeDispose, readonly, shallowRef, toRaw } from 'vue-demi'
-import type { Ref } from 'vue-demi'
+import { onScopeDispose, readonly, shallowRef, toRaw } from 'vue'
+import type { Ref } from 'vue'
export interface UseSelectorOptions {
compare?: (a: TSelected, b: TSelected) => boolean
diff --git a/packages/vue-store/src/useStore.ts b/packages/vue-store/src/useStore.ts
index 7b6d1346..7a273d8b 100644
--- a/packages/vue-store/src/useStore.ts
+++ b/packages/vue-store/src/useStore.ts
@@ -1,5 +1,5 @@
import { useSelector } from './useSelector'
-import type { Ref } from 'vue-demi'
+import type { Ref } from 'vue'
/**
* Deprecated alias for {@link useSelector}.
diff --git a/packages/vue-store/tests/index.test.tsx b/packages/vue-store/tests/index.test.tsx
index f9a2d0ce..a2a7c678 100644
--- a/packages/vue-store/tests/index.test.tsx
+++ b/packages/vue-store/tests/index.test.tsx
@@ -1,5 +1,5 @@
import { describe, expect, it, test, vi } from 'vitest'
-import { defineComponent, h } from 'vue-demi'
+import { defineComponent, h } from 'vue'
import { render, waitFor } from '@testing-library/vue'
import { createAtom, createStore } from '@tanstack/store'
import { userEvent } from '@testing-library/user-event'
diff --git a/packages/vue-store/tests/test.test-d.ts b/packages/vue-store/tests/test.test-d.ts
index 77410538..6a1c9151 100644
--- a/packages/vue-store/tests/test.test-d.ts
+++ b/packages/vue-store/tests/test.test-d.ts
@@ -1,7 +1,7 @@
import { expectTypeOf, test } from 'vitest'
import { createAtom, createStore } from '@tanstack/store'
import { _useStore, useAtom, useSelector, useStore } from '../src'
-import type { Ref } from 'vue-demi'
+import type { Ref } from 'vue'
import type { Store } from '@tanstack/store'
test('useSelector works with derived state', () => {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5abe8a24..426062e7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -162,7 +162,7 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
-packageExtensionsChecksum: sha256-QSjdHvOExHHSyNiVe6itFpAjt52AQVWz92MEbXECXgk=
+packageExtensionsChecksum: sha256-iWMlPJFHrj45xVLCE2lxHogmkbbHqhVgFYbuj0g0l1k=
importers:
@@ -767,7 +767,7 @@ importers:
examples/react/atoms:
dependencies:
'@tanstack/react-store':
- specifier: ^0.11.2
+ specifier: ^1.0.0-alpha.0
version: link:../../../packages/react-store
react:
specifier: ^19.2.5
@@ -792,7 +792,7 @@ importers:
examples/react/simple:
dependencies:
'@tanstack/react-store':
- specifier: ^0.11.2
+ specifier: ^1.0.0-alpha.0
version: link:../../../packages/react-store
react:
specifier: ^19.2.5
@@ -817,7 +817,7 @@ importers:
examples/react/store-actions:
dependencies:
'@tanstack/react-store':
- specifier: ^0.11.2
+ specifier: ^1.0.0-alpha.0
version: link:../../../packages/react-store
react:
specifier: ^19.2.5
@@ -842,7 +842,7 @@ importers:
examples/react/store-context:
dependencies:
'@tanstack/react-store':
- specifier: ^0.11.2
+ specifier: ^1.0.0-alpha.0
version: link:../../../packages/react-store
react:
specifier: ^19.2.5
@@ -867,7 +867,7 @@ importers:
examples/react/stores:
dependencies:
'@tanstack/react-store':
- specifier: ^0.11.2
+ specifier: ^1.0.0-alpha.0
version: link:../../../packages/react-store
react:
specifier: ^19.2.5
@@ -1127,7 +1127,7 @@ importers:
examples/vue/atoms:
dependencies:
'@tanstack/vue-store':
- specifier: ^0.11.2
+ specifier: ^1.0.0-alpha.0
version: link:../../../packages/vue-store
vue:
specifier: ^3.5.32
@@ -1149,7 +1149,7 @@ importers:
examples/vue/simple:
dependencies:
'@tanstack/vue-store':
- specifier: ^0.11.2
+ specifier: ^1.0.0-alpha.0
version: link:../../../packages/vue-store
vue:
specifier: ^3.5.32
@@ -1171,7 +1171,7 @@ importers:
examples/vue/store-actions:
dependencies:
'@tanstack/vue-store':
- specifier: ^0.11.2
+ specifier: ^1.0.0-alpha.0
version: link:../../../packages/vue-store
vue:
specifier: ^3.5.32
@@ -1193,7 +1193,7 @@ importers:
examples/vue/store-context:
dependencies:
'@tanstack/vue-store':
- specifier: ^0.11.2
+ specifier: ^1.0.0-alpha.0
version: link:../../../packages/vue-store
vue:
specifier: ^3.5.32
@@ -1215,7 +1215,7 @@ importers:
examples/vue/stores:
dependencies:
'@tanstack/vue-store':
- specifier: ^0.11.2
+ specifier: ^1.0.0-alpha.0
version: link:../../../packages/vue-store
vue:
specifier: ^3.5.32
@@ -1318,9 +1318,6 @@ importers:
'@tanstack/store':
specifier: workspace:*
version: link:../store
- use-sync-external-store:
- specifier: ^1.6.0
- version: 1.6.0(react@19.2.5)
devDependencies:
'@testing-library/react':
specifier: ^16.3.2
@@ -1331,9 +1328,6 @@ importers:
'@types/react-dom':
specifier: ^19.2.3
version: 19.2.3(@types/react@19.2.17)
- '@types/use-sync-external-store':
- specifier: ^1.5.0
- version: 1.5.0
'@vitejs/plugin-react':
specifier: ^6.0.1
version: 6.0.1(vite@8.3.0(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.4.2)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.0)(yaml@2.9.0))
@@ -1405,9 +1399,6 @@ importers:
'@tanstack/store':
specifier: workspace:*
version: link:../store
- vue-demi:
- specifier: ^0.14.10
- version: 0.14.10(@vue/composition-api@1.7.2(vue@3.5.43(typescript@6.0.3)))(vue@3.5.43(typescript@6.0.3))
devDependencies:
'@testing-library/vue':
specifier: ^8.1.0
@@ -1415,18 +1406,9 @@ importers:
'@vitejs/plugin-vue':
specifier: ^6.0.5
version: 6.0.5(vite@8.3.0(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.4.2)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.0)(yaml@2.9.0))(vue@3.5.43(typescript@6.0.3))
- '@vue/composition-api':
- specifier: ^1.7.2
- version: 1.7.2(vue@3.5.43(typescript@6.0.3))
vue:
specifier: ^3.5.43
version: 3.5.43(typescript@6.0.3)
- vue2:
- specifier: npm:vue@2.6
- version: vue@2.6.14
- vue2.7:
- specifier: npm:vue@2.7
- version: vue@2.7.16
packages:
@@ -4301,9 +4283,6 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
- '@types/use-sync-external-store@1.5.0':
- resolution: {integrity: sha512-5dyB8nLC/qogMrlCizZnYWQTA4lnb/v+It+sqNl5YnSRAPMlIqY/X0Xn+gZw8vOL+TgTTr28VEbn3uf8fUtAkw==}
-
'@typescript-eslint/eslint-plugin@8.58.1':
resolution: {integrity: sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -4552,9 +4531,6 @@ packages:
'@vue/compiler-dom@3.5.43':
resolution: {integrity: sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==}
- '@vue/compiler-sfc@2.7.16':
- resolution: {integrity: sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==}
-
'@vue/compiler-sfc@3.5.32':
resolution: {integrity: sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==}
@@ -4567,11 +4543,6 @@ packages:
'@vue/compiler-ssr@3.5.43':
resolution: {integrity: sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==}
- '@vue/composition-api@1.7.2':
- resolution: {integrity: sha512-M8jm9J/laYrYT02665HkZ5l2fWTK4dcVg3BsDHm/pfz+MjDYwX+9FUaZyGwEyXEDonQYRCo0H7aLgdklcIELjw==}
- peerDependencies:
- vue: '>= 2.5 < 2.7'
-
'@vue/language-core@3.2.6':
resolution: {integrity: sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==}
@@ -6641,11 +6612,6 @@ packages:
prettier: ^3.0.0
svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0
- prettier@2.8.8:
- resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
- engines: {node: '>=10.13.0'}
- hasBin: true
-
prettier@3.8.2:
resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==}
engines: {node: '>=14'}
@@ -7477,11 +7443,6 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
- use-sync-external-store@1.6.0:
- resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
-
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -7653,17 +7614,6 @@ packages:
vue-component-type-helpers@2.2.12:
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
- vue-demi@0.14.10:
- resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
- engines: {node: '>=12'}
- hasBin: true
- peerDependencies:
- '@vue/composition-api': ^1.0.0-rc.1
- vue: ^3.0.0-0 || ^2.6.0
- peerDependenciesMeta:
- '@vue/composition-api':
- optional: true
-
vue-eslint-parser@10.4.0:
resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -7676,14 +7626,6 @@ packages:
peerDependencies:
typescript: '>=5.0.0'
- vue@2.6.14:
- resolution: {integrity: sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==}
- deprecated: Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.
-
- vue@2.7.16:
- resolution: {integrity: sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==}
- deprecated: Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.
-
vue@3.5.32:
resolution: {integrity: sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==}
peerDependencies:
@@ -10305,8 +10247,6 @@ snapshots:
'@types/unist@3.0.3': {}
- '@types/use-sync-external-store@1.5.0': {}
-
'@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -10667,14 +10607,6 @@ snapshots:
'@vue/compiler-core': 3.5.43
'@vue/shared': 3.5.43
- '@vue/compiler-sfc@2.7.16':
- dependencies:
- '@babel/parser': 7.29.8
- postcss: 8.5.28
- source-map: 0.6.1
- optionalDependencies:
- prettier: 2.8.8
-
'@vue/compiler-sfc@3.5.32':
dependencies:
'@babel/parser': 7.29.8
@@ -10709,10 +10641,6 @@ snapshots:
'@vue/compiler-dom': 3.5.43
'@vue/shared': 3.5.43
- '@vue/composition-api@1.7.2(vue@3.5.43(typescript@6.0.3))':
- dependencies:
- vue: 3.5.43(typescript@6.0.3)
-
'@vue/language-core@3.2.6':
dependencies:
'@volar/language-core': 2.4.28
@@ -13376,9 +13304,6 @@ snapshots:
prettier: 3.8.2
svelte: 5.55.7(@typescript-eslint/types@8.58.1)
- prettier@2.8.8:
- optional: true
-
prettier@3.8.2: {}
pretty-format@27.5.1:
@@ -14301,10 +14226,6 @@ snapshots:
dependencies:
punycode: 2.3.1
- use-sync-external-store@1.6.0(react@19.2.5):
- dependencies:
- react: 19.2.5
-
util-deprecate@1.0.2: {}
validate-npm-package-name@8.0.0: {}
@@ -14490,12 +14411,6 @@ snapshots:
vue-component-type-helpers@2.2.12: {}
- vue-demi@0.14.10(@vue/composition-api@1.7.2(vue@3.5.43(typescript@6.0.3)))(vue@3.5.43(typescript@6.0.3)):
- dependencies:
- vue: 3.5.43(typescript@6.0.3)
- optionalDependencies:
- '@vue/composition-api': 1.7.2(vue@3.5.43(typescript@6.0.3))
-
vue-eslint-parser@10.4.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
dependencies:
debug: 4.4.3(supports-color@7.2.0)
@@ -14514,13 +14429,6 @@ snapshots:
'@vue/language-core': 3.2.6
typescript: 6.0.3
- vue@2.6.14: {}
-
- vue@2.7.16:
- dependencies:
- '@vue/compiler-sfc': 2.7.16
- csstype: 3.2.3
-
vue@3.5.32(typescript@6.0.3):
dependencies:
'@vue/compiler-dom': 3.5.32
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 8c502004..48f28675 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -10,14 +10,7 @@ trustPolicyExclude:
- 'semver@5.7.2' # Socket: popular legacy line, 673M weekly downloads; alerts page returned no alert entries
- 'semver@6.3.1' # Socket: popular latest-6 line, 673M weekly downloads; alerts page returned no alert entries
-peerDependencyRules:
- allowedVersions:
- '@vue/composition-api>vue': '>= 2.5'
-
packageExtensions:
- '@vue/composition-api':
- peerDependencies:
- vue: '>= 2.5'
'@vue/test-utils@2.4.6':
peerDependencies:
# Use the consumer's Vue runtime for rendering and reactivity.
@@ -44,9 +37,6 @@ allowBuilds:
'@parcel/watcher': false # optional dep of @angular/build
lmdb: false # optional dep of @angular/build
msgpackr-extract: false # optional dep of @angular/build
-
- # @tanstack/vue-store
- vue-demi: true # only required for vue 2 support
minimumReleaseAgeExclude:
# Renovate security update: nx@22.7.7 || 22.7.10
- nx@22.7.7 || 22.7.10