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 {value} + } + + 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