diff --git a/docs/framework/angular/reference/functions/injectStore.md b/docs/framework/angular/reference/functions/injectStore.md index 91883003..25d7acf3 100644 --- a/docs/framework/angular/reference/functions/injectStore.md +++ b/docs/framework/angular/reference/functions/injectStore.md @@ -14,7 +14,7 @@ function injectStore( options?): Signal; ``` -Defined in: [index.ts:19](https://github.com/TanStack/store/blob/main/packages/angular-store/src/index.ts#L19) +Defined in: [index.ts:21](https://github.com/TanStack/store/blob/main/packages/angular-store/src/index.ts#L21) ### Type Parameters @@ -30,7 +30,7 @@ Defined in: [index.ts:19](https://github.com/TanStack/store/blob/main/packages/a #### store -`Store`\<`TState`, `any`\> +`Atom`\<`TState`\> #### selector? @@ -53,7 +53,7 @@ function injectStore( options?): Signal; ``` -Defined in: [index.ts:24](https://github.com/TanStack/store/blob/main/packages/angular-store/src/index.ts#L24) +Defined in: [index.ts:26](https://github.com/TanStack/store/blob/main/packages/angular-store/src/index.ts#L26) ### Type Parameters @@ -69,7 +69,7 @@ Defined in: [index.ts:24](https://github.com/TanStack/store/blob/main/packages/a #### store -`Derived`\<`TState`, `any`\> +`Atom`\<`TState`\> | `ReadonlyAtom`\<`TState`\> #### selector? diff --git a/docs/framework/preact/reference/functions/shallow.md b/docs/framework/preact/reference/functions/shallow.md index 8284ccaf..447361a3 100644 --- a/docs/framework/preact/reference/functions/shallow.md +++ b/docs/framework/preact/reference/functions/shallow.md @@ -9,7 +9,7 @@ title: shallow function shallow(objA, objB): boolean; ``` -Defined in: [index.ts:130](https://github.com/TanStack/store/blob/main/packages/preact-store/src/index.ts#L130) +Defined in: [index.ts:121](https://github.com/TanStack/store/blob/main/packages/preact-store/src/index.ts#L121) ## Type Parameters diff --git a/docs/framework/preact/reference/functions/useStore.md b/docs/framework/preact/reference/functions/useStore.md index 22d4ed55..43e5e115 100644 --- a/docs/framework/preact/reference/functions/useStore.md +++ b/docs/framework/preact/reference/functions/useStore.md @@ -5,80 +5,39 @@ title: useStore # Function: useStore() -## Call Signature - -```ts -function useStore( - store, - selector?, - options?): TSelected; -``` - -Defined in: [index.ts:104](https://github.com/TanStack/store/blob/main/packages/preact-store/src/index.ts#L104) - -### Type Parameters - -#### TState - -`TState` - -#### TSelected - -`TSelected` = `NoInfer`\<`TState`\> - -### Parameters - -#### store - -`Store`\<`TState`, `any`\> - -#### selector? - -(`state`) => `TSelected` - -#### options? - -`UseStoreOptions`\<`TSelected`\> - -### Returns - -`TSelected` - -## Call Signature - ```ts function useStore( store, - selector?, - options?): TSelected; + selector, + options): TSelected; ``` -Defined in: [index.ts:109](https://github.com/TanStack/store/blob/main/packages/preact-store/src/index.ts#L109) +Defined in: [index.ts:105](https://github.com/TanStack/store/blob/main/packages/preact-store/src/index.ts#L105) -### Type Parameters +## Type Parameters -#### TState +### TState `TState` -#### TSelected +### TSelected `TSelected` = `NoInfer`\<`TState`\> -### Parameters +## Parameters -#### store +### store -`Derived`\<`TState`, `any`\> +`Atom`\<`TState`\> | `ReadonlyAtom`\<`TState`\> -#### selector? +### selector (`state`) => `TSelected` -#### options? +### options -`UseStoreOptions`\<`TSelected`\> +`UseStoreOptions`\<`TSelected`\> = `{}` -### Returns +## Returns `TSelected` diff --git a/docs/framework/solid/reference/functions/shallow.md b/docs/framework/solid/reference/functions/shallow.md index 4f071a7a..47dd9156 100644 --- a/docs/framework/solid/reference/functions/shallow.md +++ b/docs/framework/solid/reference/functions/shallow.md @@ -9,7 +9,7 @@ title: shallow function shallow(objA, objB): boolean; ``` -Defined in: [index.tsx:49](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L49) +Defined in: [index.tsx:39](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L39) ## Type Parameters diff --git a/docs/framework/solid/reference/functions/useStore.md b/docs/framework/solid/reference/functions/useStore.md index dc667d6b..f3f14328 100644 --- a/docs/framework/solid/reference/functions/useStore.md +++ b/docs/framework/solid/reference/functions/useStore.md @@ -5,80 +5,39 @@ title: useStore # Function: useStore() -## Call Signature - ```ts function useStore( store, - selector?, -options?): Accessor; + selector, +options): Accessor; ``` Defined in: [index.tsx:16](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L16) -### Type Parameters - -#### TState - -`TState` - -#### TSelected - -`TSelected` = `NoInfer`\<`TState`\> - -### Parameters - -#### store - -`Store`\<`TState`, `any`\> - -#### selector? - -(`state`) => `TSelected` - -#### options? - -`UseStoreOptions`\<`TSelected`\> - -### Returns - -`Accessor`\<`TSelected`\> - -## Call Signature - -```ts -function useStore( - store, - selector?, -options?): Accessor; -``` - -Defined in: [index.tsx:21](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L21) - -### Type Parameters +## Type Parameters -#### TState +### TState `TState` -#### TSelected +### TSelected `TSelected` = `NoInfer`\<`TState`\> -### Parameters +## Parameters -#### store +### store -`Derived`\<`TState`, `any`\> +`Atom`\<`TState`\> | `ReadonlyAtom`\<`TState`\> -#### selector? +### selector (`state`) => `TSelected` -#### options? +### options -`UseStoreOptions`\<`TSelected`\> +`UseStoreOptions`\<`TSelected`\> = `{}` -### Returns +## Returns `Accessor`\<`TSelected`\> diff --git a/docs/framework/svelte/reference/functions/shallow.md b/docs/framework/svelte/reference/functions/shallow.md index 0fcd2973..dcff0745 100644 --- a/docs/framework/svelte/reference/functions/shallow.md +++ b/docs/framework/svelte/reference/functions/shallow.md @@ -9,7 +9,7 @@ title: shallow function shallow(objA, objB): boolean; ``` -Defined in: [index.svelte.ts:51](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L51) +Defined in: [index.svelte.ts:41](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L41) ## Type Parameters diff --git a/docs/framework/svelte/reference/functions/useStore.md b/docs/framework/svelte/reference/functions/useStore.md index cb2b354b..0f47b631 100644 --- a/docs/framework/svelte/reference/functions/useStore.md +++ b/docs/framework/svelte/reference/functions/useStore.md @@ -5,91 +5,44 @@ title: useStore # Function: useStore() -## Call Signature - ```ts function useStore( store, - selector?, - options?): object; + selector, + options): object; ``` Defined in: [index.svelte.ts:14](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L14) -### Type Parameters - -#### TState - -`TState` - -#### TSelected - -`TSelected` = `NoInfer`\<`TState`\> - -### Parameters - -#### store - -`Store`\<`TState`, `any`\> - -#### selector? - -(`state`) => `TSelected` - -#### options? - -`UseStoreOptions`\<`TSelected`\> - -### Returns - -`object` - -#### current - -```ts -readonly current: TSelected; -``` - -## Call Signature - -```ts -function useStore( - store, - selector?, - options?): object; -``` - -Defined in: [index.svelte.ts:19](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L19) - -### Type Parameters +## Type Parameters -#### TState +### TState `TState` -#### TSelected +### TSelected `TSelected` = `NoInfer`\<`TState`\> -### Parameters +## Parameters -#### store +### store -`Derived`\<`TState`, `any`\> +`Atom`\<`TState`\> | `ReadonlyAtom`\<`TState`\> -#### selector? +### selector (`state`) => `TSelected` -#### options? +### options -`UseStoreOptions`\<`TSelected`\> +`UseStoreOptions`\<`TSelected`\> = `{}` -### Returns +## Returns `object` -#### current +### current ```ts readonly current: TSelected; diff --git a/docs/framework/vue/reference/functions/shallow.md b/docs/framework/vue/reference/functions/shallow.md index 48ef7d1e..4381e76a 100644 --- a/docs/framework/vue/reference/functions/shallow.md +++ b/docs/framework/vue/reference/functions/shallow.md @@ -9,7 +9,7 @@ title: shallow function shallow(objA, objB): boolean; ``` -Defined in: [index.ts:55](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L55) +Defined in: [index.ts:45](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L45) ## Type Parameters diff --git a/docs/framework/vue/reference/functions/useStore.md b/docs/framework/vue/reference/functions/useStore.md index fb924f0c..978581ca 100644 --- a/docs/framework/vue/reference/functions/useStore.md +++ b/docs/framework/vue/reference/functions/useStore.md @@ -5,80 +5,39 @@ title: useStore # Function: useStore() -## Call Signature - ```ts function useStore( store, - selector?, -options?): Readonly>; + selector, +options): Readonly>; ``` Defined in: [index.ts:16](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L16) -### Type Parameters - -#### TState - -`TState` - -#### TSelected - -`TSelected` = `NoInfer`\<`TState`\> - -### Parameters - -#### store - -`Store`\<`TState`, `any`\> - -#### selector? - -(`state`) => `TSelected` - -#### options? - -`UseStoreOptions`\<`TSelected`\> - -### Returns - -`Readonly`\<`Ref`\<`TSelected`\>\> - -## Call Signature - -```ts -function useStore( - store, - selector?, -options?): Readonly>; -``` - -Defined in: [index.ts:21](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L21) - -### Type Parameters +## Type Parameters -#### TState +### TState `TState` -#### TSelected +### TSelected `TSelected` = `NoInfer`\<`TState`\> -### Parameters +## Parameters -#### store +### store -`Derived`\<`TState`, `any`\> +`Atom`\<`TState`\> | `ReadonlyAtom`\<`TState`\> -#### selector? +### selector (`state`) => `TSelected` -#### options? +### options -`UseStoreOptions`\<`TSelected`\> +`UseStoreOptions`\<`TSelected`\> = `{}` -### Returns +## Returns `Readonly`\<`Ref`\<`TSelected`\>\> diff --git a/packages/angular-store/src/index.ts b/packages/angular-store/src/index.ts index 130120a4..b29aaae1 100644 --- a/packages/angular-store/src/index.ts +++ b/packages/angular-store/src/index.ts @@ -6,9 +6,11 @@ import { linkedSignal, runInInjectionContext, } from '@angular/core' -import type { Derived, Store } from '@tanstack/store' +import type { Atom, ReadonlyAtom } from '@tanstack/store' import type { CreateSignalOptions, Signal } from '@angular/core' +type StoreContext = Record + export * from '@tanstack/store' /** @@ -17,17 +19,20 @@ export * from '@tanstack/store' type NoInfer = [T][T extends any ? 0 : never] export function injectStore>( - store: Store, + store: Atom, selector?: (state: NoInfer) => TSelected, options?: CreateSignalOptions & { injector?: Injector }, ): Signal export function injectStore>( - store: Derived, + store: Atom | ReadonlyAtom, selector?: (state: NoInfer) => TSelected, options?: CreateSignalOptions & { injector?: Injector }, ): Signal -export function injectStore>( - store: Store | Derived, +export function injectStore< + TState extends StoreContext, + TSelected = NoInfer, +>( + store: Atom | ReadonlyAtom, selector: (state: NoInfer) => TSelected = (d) => d as TSelected, options: CreateSignalOptions & { injector?: Injector } = { equal: shallow, @@ -41,10 +46,10 @@ export function injectStore>( return runInInjectionContext(options.injector, () => { const destroyRef = inject(DestroyRef) - const slice = linkedSignal(() => selector(store.state), options) + const slice = linkedSignal(() => selector(store.get()), options) - const unsubscribe = store.subscribe(() => { - slice.set(selector(store.state)) + const { unsubscribe } = store.subscribe((s) => { + slice.set(selector(s)) }) destroyRef.onDestroy(() => { @@ -95,10 +100,10 @@ function shallow(objA: T, objB: T) { return false } - for (let i = 0; i < keysA.length; i++) { + for (const key of keysA) { if ( - !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) || - !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T]) + !Object.prototype.hasOwnProperty.call(objB, key) || + !Object.is(objA[key as keyof T], objB[key as keyof T]) ) { return false } diff --git a/packages/angular-store/tests/index.test.ts b/packages/angular-store/tests/index.test.ts index 17c5e465..65ea83f9 100644 --- a/packages/angular-store/tests/index.test.ts +++ b/packages/angular-store/tests/index.test.ts @@ -2,12 +2,12 @@ import { describe, expect, test } from 'vitest' import { Component, effect } from '@angular/core' import { TestBed } from '@angular/core/testing' import { By } from '@angular/platform-browser' -import { Store } from '@tanstack/store' +import { createAtom } from '@tanstack/store' import { injectStore } from '../src/index' describe('injectStore', () => { test(`allows us to select state using a selector`, () => { - const store = new Store({ select: 0, ignored: 1 }) + const store = createAtom({ select: 0, ignored: 1 }) @Component({ template: `

Store: {{ storeVal() }}

`, @@ -25,7 +25,7 @@ describe('injectStore', () => { }) test('only triggers a re-render when selector state is updated', () => { - const store = new Store({ select: 0, ignored: 1 }) + const store = createAtom({ select: 0, ignored: 1 }) let count = 0 @Component({ @@ -53,14 +53,14 @@ describe('injectStore', () => { } updateSelect() { - store.setState((v) => ({ + store.set((v) => ({ ...v, select: 10, })) } updateIgnored() { - store.setState((v) => ({ + store.set((v) => ({ ...v, ignored: 10, })) @@ -96,7 +96,7 @@ describe('injectStore', () => { describe('dataType', () => { test('date change trigger re-render', () => { - const store = new Store({ date: new Date('2025-03-29T21:06:30.401Z') }) + const store = createAtom({ date: new Date('2025-03-29T21:06:30.401Z') }) @Component({ template: ` @@ -117,7 +117,7 @@ describe('dataType', () => { } updateDate() { - store.setState((v) => ({ + store.set((v) => ({ ...v, date: new Date('2025-03-29T21:06:40.401Z'), })) diff --git a/packages/angular-store/tests/test.test-d.ts b/packages/angular-store/tests/test.test-d.ts index 414ae52b..1b8e28a8 100644 --- a/packages/angular-store/tests/test.test-d.ts +++ b/packages/angular-store/tests/test.test-d.ts @@ -1,20 +1,16 @@ import { expectTypeOf, test } from 'vitest' -import { Derived, Store, injectStore } from '../src' +import { createAtom } from '@tanstack/store' +import { injectStore } from '../src' import type { Signal } from '@angular/core' test('injectStore works with derived state', () => { - const store = new Store(12) - const derived = new Derived({ - deps: [store], - fn: () => { - return { val: store.state * 2 } - }, - }) + const store = createAtom(12) + const derived = createAtom(() => store.get() * 2) const val = injectStore(derived, (state) => { - expectTypeOf(state).toMatchTypeOf<{ val: number }>() - return state.val + expectTypeOf(state).toEqualTypeOf() + return state }) - expectTypeOf(val).toMatchTypeOf>() + expectTypeOf(val).toEqualTypeOf>() }) diff --git a/packages/preact-store/src/index.ts b/packages/preact-store/src/index.ts index 7b2c5bcc..00da4858 100644 --- a/packages/preact-store/src/index.ts +++ b/packages/preact-store/src/index.ts @@ -1,5 +1,5 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks' -import type { Derived, Store } from '@tanstack/store' +import type { Atom, ReadonlyAtom } from '@tanstack/store' export * from '@tanstack/store' @@ -102,24 +102,14 @@ function useSyncExternalStoreWithSelector( } export function useStore>( - store: Store, - selector?: (state: NoInfer) => TSelected, - options?: UseStoreOptions, -): TSelected -export function useStore>( - store: Derived, - selector?: (state: NoInfer) => TSelected, - options?: UseStoreOptions, -): TSelected -export function useStore>( - store: Store | Derived, + store: Atom | ReadonlyAtom, selector: (state: NoInfer) => TSelected = (d) => d as any, options: UseStoreOptions = {}, ): TSelected { const equal = options.equal ?? shallow const slice = useSyncExternalStoreWithSelector( - store.subscribe, - () => store.state, + (onStoreChange) => store.subscribe(onStoreChange).unsubscribe, + () => store.get(), selector, equal, ) diff --git a/packages/preact-store/tests/index.test.tsx b/packages/preact-store/tests/index.test.tsx index 06315d51..5594f2fc 100644 --- a/packages/preact-store/tests/index.test.tsx +++ b/packages/preact-store/tests/index.test.tsx @@ -1,15 +1,15 @@ import { describe, expect, it, test, vi } from 'vitest' import { render, waitFor } from '@testing-library/preact' -import { Derived, Store } from '@tanstack/store' import { useState } from 'preact/hooks' import { userEvent } from '@testing-library/user-event' +import { createAtom } from '@tanstack/store' import { shallow, useStore } from '../src/index' const user = userEvent.setup() describe('useStore', () => { it('allows us to select state using a selector', () => { - const store = new Store({ + const store = createAtom({ select: 0, ignored: 1, }) @@ -26,7 +26,7 @@ describe('useStore', () => { // This should ideally test the custom uSES hook it('only triggers a re-render when selector state is updated', async () => { - const store = new Store({ + const store = createAtom({ select: 0, ignored: 1, }) @@ -43,7 +43,7 @@ describe('useStore', () => { diff --git a/packages/preact-store/tests/test.test-d.ts b/packages/preact-store/tests/test.test-d.ts index 24093bd0..e23959b8 100644 --- a/packages/preact-store/tests/test.test-d.ts +++ b/packages/preact-store/tests/test.test-d.ts @@ -1,19 +1,15 @@ import { expectTypeOf, test } from 'vitest' -import { Derived, Store, useStore } from '../src' +import { createAtom } from '@tanstack/store' +import { useStore } from '../src' test('useStore works with derived state', () => { - const store = new Store(12) - const derived = new Derived({ - deps: [store], - fn: () => { - return { val: store.state * 2 } - }, - }) + const store = createAtom(12) + const derived = createAtom(() => store.get() * 2) const val = useStore(derived, (state) => { - expectTypeOf(state).toMatchTypeOf<{ val: number }>() - return state.val + expectTypeOf(state).toEqualTypeOf() + return state }) - expectTypeOf(val).toMatchTypeOf() + expectTypeOf(val).toEqualTypeOf() }) diff --git a/packages/react-store/src/index.ts b/packages/react-store/src/index.ts index 21d3a478..1c8fec9b 100644 --- a/packages/react-store/src/index.ts +++ b/packages/react-store/src/index.ts @@ -3,6 +3,8 @@ import type { Derived, Store } from '@tanstack/store' export * from '@tanstack/store' +export { useSelector } from './useSelector' + /** * @private */ diff --git a/packages/react-store/src/useSelector.ts b/packages/react-store/src/useSelector.ts new file mode 100644 index 00000000..c3e87e0d --- /dev/null +++ b/packages/react-store/src/useSelector.ts @@ -0,0 +1,44 @@ +import { useCallback } from 'react' +import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector' +import type { AnyAtom } from '@tanstack/store' + +type SyncExternalStoreSubscribe = Parameters< + typeof useSyncExternalStoreWithSelector +>[0] + +function defaultCompare(a: T, b: T) { + return a === b +} + +export function useSelector( + atom: TAtom, + selector: ( + snapshot: TAtom extends { get: () => infer TSnapshot } + ? TSnapshot + : undefined, + ) => T, + compare: (a: T, b: T) => boolean = defaultCompare, +): T { + const subscribe: SyncExternalStoreSubscribe = useCallback( + (handleStoreChange) => { + if (!atom) { + return () => {} + } + const { unsubscribe } = atom.subscribe(handleStoreChange) + return unsubscribe + }, + [atom], + ) + + const boundGetSnapshot = useCallback(() => atom?.get(), [atom]) + + const selectedSnapshot = useSyncExternalStoreWithSelector( + subscribe, + boundGetSnapshot, + boundGetSnapshot, + selector, + compare, + ) + + return selectedSnapshot +} diff --git a/packages/react-store/tests/index.test.tsx b/packages/react-store/tests/index.test.tsx index 37d1d37c..4ad996a5 100644 --- a/packages/react-store/tests/index.test.tsx +++ b/packages/react-store/tests/index.test.tsx @@ -1,21 +1,22 @@ import { describe, expect, it, test, vi } from 'vitest' import { render, waitFor } from '@testing-library/react' -import { Derived, Store } from '@tanstack/store' import { useState } from 'react' import { userEvent } from '@testing-library/user-event' -import { shallow, useStore } from '../src/index' +import { createAtom } from '@tanstack/store' +import { shallow, useSelector } from '../src/index' const user = userEvent.setup() describe('useStore', () => { it('allows us to select state using a selector', () => { - const store = new Store({ + const store = createAtom({ select: 0, ignored: 1, }) function Comp() { - const storeVal = useStore(store, (state) => state.select) + // const storeVal = useStore(store, (state) => state.select) + const storeVal = useSelector(store, (s) => s.select) return

Store: {storeVal}

} @@ -25,13 +26,14 @@ describe('useStore', () => { }) it('only triggers a re-render when selector state is updated', async () => { - const store = new Store({ + const store = createAtom({ select: 0, ignored: 1, }) function Comp() { - const storeVal = useStore(store, (state) => state.select) + // const storeVal = useStore(store, (state) => state.select) + const storeVal = useSelector(store, (s) => s.select) const [fn] = useState(vi.fn) fn() @@ -42,7 +44,7 @@ describe('useStore', () => { diff --git a/packages/solid-store/src/index.tsx b/packages/solid-store/src/index.tsx index 104653b8..48bfe9c0 100644 --- a/packages/solid-store/src/index.tsx +++ b/packages/solid-store/src/index.tsx @@ -1,6 +1,6 @@ import { createSignal, onCleanup } from 'solid-js' -import type { Derived, Store } from '@tanstack/store' import type { Accessor } from 'solid-js' +import type { Atom, ReadonlyAtom } from '@tanstack/store' export * from '@tanstack/store' @@ -14,30 +14,20 @@ interface UseStoreOptions { } export function useStore>( - store: Store, - selector?: (state: NoInfer) => TSelected, - options?: UseStoreOptions, -): Accessor -export function useStore>( - store: Derived, - selector?: (state: NoInfer) => TSelected, - options?: UseStoreOptions, -): Accessor -export function useStore>( - store: Store | Derived, + store: Atom | ReadonlyAtom, selector: (state: NoInfer) => TSelected = (d) => d as any, options: UseStoreOptions = {}, ): Accessor { - const [signal, setSignal] = createSignal(selector(store.state)) + const [signal, setSignal] = createSignal(selector(store.get())) const equal = options.equal ?? shallow - const unsub = store.subscribe(() => { - const data = selector(store.state) + const unsub = store.subscribe((s) => { + const data = selector(s) if (equal(signal(), data)) { return } setSignal(() => data) - }) + }).unsubscribe onCleanup(() => { unsub() diff --git a/packages/solid-store/tests/index.test.tsx b/packages/solid-store/tests/index.test.tsx index 2efa725c..fb537c51 100644 --- a/packages/solid-store/tests/index.test.tsx +++ b/packages/solid-store/tests/index.test.tsx @@ -1,11 +1,11 @@ import { describe, expect, it } from 'vitest' import { render, renderHook } from '@solidjs/testing-library' -import { Store } from '@tanstack/store' +import { createAtom } from '@tanstack/store' import { useStore } from '../src/index' describe('useStore', () => { it.todo('allows us to select state using a selector', () => { - const store = new Store({ + const store = createAtom({ select: 0, ignored: 1, }) @@ -21,7 +21,7 @@ describe('useStore', () => { }) it('allows us to select state using a selector', () => { - const store = new Store({ + const store = createAtom({ select: 0, ignored: 1, }) @@ -34,21 +34,21 @@ describe('useStore', () => { }) it('updates accessor value when state is updated', () => { - const store = new Store(0) + const store = createAtom(0) const { result } = renderHook(() => useStore(store)) - store.setState((prev) => prev + 1) + store.set((prev) => prev + 1) expect(result()).toBe(1) }) it('updates when date changes', () => { - const store = new Store(new Date('2025-03-29T21:06:30.401Z')) + const store = createAtom(new Date('2025-03-29T21:06:30.401Z')) const { result } = renderHook(() => useStore(store)) - store.setState(() => new Date('2025-03-29T21:06:40.401Z')) + store.set(() => new Date('2025-03-29T21:06:40.401Z')) expect(result()).toStrictEqual(new Date('2025-03-29T21:06:40.401Z')) }) diff --git a/packages/solid-store/tests/test.test-d.ts b/packages/solid-store/tests/test.test-d.ts index d02e4aba..f11ef88f 100644 --- a/packages/solid-store/tests/test.test-d.ts +++ b/packages/solid-store/tests/test.test-d.ts @@ -1,20 +1,16 @@ import { expectTypeOf, test } from 'vitest' -import { Derived, Store, useStore } from '../src' +import { createAtom } from '@tanstack/store' +import { useStore } from '../src' import type { Accessor } from 'solid-js' test('useStore works with derived state', () => { - const store = new Store(12) - const derived = new Derived({ - deps: [store], - fn: () => { - return { val: store.state * 2 } - }, - }) + const store = createAtom(12) + const derived = createAtom(() => store.get() * 2) const val = useStore(derived, (state) => { - expectTypeOf(state).toMatchTypeOf<{ val: number }>() - return state.val + expectTypeOf(state).toEqualTypeOf() + return state }) - expectTypeOf(val).toMatchTypeOf>() + expectTypeOf(val).toEqualTypeOf>() }) diff --git a/packages/store/package.json b/packages/store/package.json index b7fe18c6..2ac22d33 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -60,5 +60,8 @@ "@preact/signals": "^1.3.2", "solid-js": "^1.9.9", "vue": "^3.5.22" + }, + "dependencies": { + "@xstate/store": "^3.13.0" } } diff --git a/packages/store/src/alien.ts b/packages/store/src/alien.ts new file mode 100644 index 00000000..9fc75067 --- /dev/null +++ b/packages/store/src/alien.ts @@ -0,0 +1,654 @@ +/* eslint-disable */ +// Adapted from Alien Signals +// https://github.com/stackblitz/alien-signals/ + +export interface ReactiveNode { + deps?: Link + depsTail?: Link + subs?: Link + subsTail?: Link + flags: ReactiveFlags +} + +export interface Link { + version: number + dep: ReactiveNode + sub: ReactiveNode + prevSub: Link | undefined + nextSub: Link | undefined + prevDep: Link | undefined + nextDep: Link | undefined +} + +interface Stack { + value: T + prev: Stack | undefined +} + +export const enum ReactiveFlags { + None = 0, + Mutable = 1, + Watching = 2, + RecursedCheck = 4, + Recursed = 8, + Dirty = 16, + Pending = 32, +} + +export function createReactiveSystem({ + update, + notify, + unwatched, +}: { + update(sub: ReactiveNode): boolean + notify(sub: ReactiveNode): void + unwatched(sub: ReactiveNode): void +}) { + return { + link, + unlink, + propagate, + checkDirty, + shallowPropagate, + } + + function link(dep: ReactiveNode, sub: ReactiveNode, version: number): void { + const prevDep = sub.depsTail + if (prevDep !== undefined && prevDep.dep === dep) { + return + } + const nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps + if (nextDep !== undefined && nextDep.dep === dep) { + nextDep.version = version + sub.depsTail = nextDep + return + } + const prevSub = dep.subsTail + if ( + prevSub !== undefined && + prevSub.version === version && + prevSub.sub === sub + ) { + return + } + const newLink = + (sub.depsTail = + dep.subsTail = + { + version, + dep, + sub, + prevDep, + nextDep, + prevSub, + nextSub: undefined, + }) + if (nextDep !== undefined) { + nextDep.prevDep = newLink + } + if (prevDep !== undefined) { + prevDep.nextDep = newLink + } else { + sub.deps = newLink + } + if (prevSub !== undefined) { + prevSub.nextSub = newLink + } else { + dep.subs = newLink + } + } + + function unlink(link: Link, sub = link.sub): Link | undefined { + const dep = link.dep + const prevDep = link.prevDep + const nextDep = link.nextDep + const nextSub = link.nextSub + const prevSub = link.prevSub + if (nextDep !== undefined) { + nextDep.prevDep = prevDep + } else { + sub.depsTail = prevDep + } + if (prevDep !== undefined) { + prevDep.nextDep = nextDep + } else { + sub.deps = nextDep + } + if (nextSub !== undefined) { + nextSub.prevSub = prevSub + } else { + dep.subsTail = prevSub + } + if (prevSub !== undefined) { + prevSub.nextSub = nextSub + } else if ((dep.subs = nextSub) === undefined) { + unwatched(dep) + } + return nextDep + } + + function propagate(link: Link): void { + let next = link.nextSub + let stack: Stack | undefined + + top: do { + const sub = link.sub + let flags = sub.flags + + if ( + !( + flags & + (ReactiveFlags.RecursedCheck | + ReactiveFlags.Recursed | + ReactiveFlags.Dirty | + ReactiveFlags.Pending) + ) + ) { + sub.flags = flags | ReactiveFlags.Pending + } else if ( + !(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed)) + ) { + flags = ReactiveFlags.None + } else if (!(flags & ReactiveFlags.RecursedCheck)) { + sub.flags = (flags & ~ReactiveFlags.Recursed) | ReactiveFlags.Pending + } else if ( + !(flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) && + isValidLink(link, sub) + ) { + sub.flags = flags | (ReactiveFlags.Recursed | ReactiveFlags.Pending) + flags &= ReactiveFlags.Mutable + } else { + flags = ReactiveFlags.None + } + + if (flags & ReactiveFlags.Watching) { + notify(sub) + } + + if (flags & ReactiveFlags.Mutable) { + const subSubs = sub.subs + if (subSubs !== undefined) { + const nextSub = (link = subSubs).nextSub + if (nextSub !== undefined) { + stack = { value: next, prev: stack } + next = nextSub + } + continue + } + } + + if ((link = next!) !== undefined) { + next = link.nextSub + continue + } + + while (stack !== undefined) { + link = stack.value! + stack = stack.prev + if (link !== undefined) { + next = link.nextSub + continue top + } + } + + break + } while (true) + } + + function checkDirty(link: Link, sub: ReactiveNode): boolean { + let stack: Stack | undefined + let checkDepth = 0 + let dirty = false + + top: do { + const dep = link.dep + const flags = dep.flags + + if (sub.flags & ReactiveFlags.Dirty) { + dirty = true + } else if ( + (flags & (ReactiveFlags.Mutable | ReactiveFlags.Dirty)) === + (ReactiveFlags.Mutable | ReactiveFlags.Dirty) + ) { + if (update(dep)) { + const subs = dep.subs! + if (subs.nextSub !== undefined) { + shallowPropagate(subs) + } + dirty = true + } + } else if ( + (flags & (ReactiveFlags.Mutable | ReactiveFlags.Pending)) === + (ReactiveFlags.Mutable | ReactiveFlags.Pending) + ) { + if (link.nextSub !== undefined || link.prevSub !== undefined) { + stack = { value: link, prev: stack } + } + link = dep.deps! + sub = dep + ++checkDepth + continue + } + + if (!dirty) { + const nextDep = link.nextDep + if (nextDep !== undefined) { + link = nextDep + continue + } + } + + while (checkDepth--) { + const firstSub = sub.subs! + const hasMultipleSubs = firstSub.nextSub !== undefined + if (hasMultipleSubs) { + link = stack!.value + stack = stack!.prev + } else { + link = firstSub + } + if (dirty) { + if (update(sub)) { + if (hasMultipleSubs) { + shallowPropagate(firstSub) + } + sub = link.sub + continue + } + dirty = false + } else { + sub.flags &= ~ReactiveFlags.Pending + } + sub = link.sub + const nextDep = link.nextDep + if (nextDep !== undefined) { + link = nextDep + continue top + } + } + + return dirty + } while (true) + } + + function shallowPropagate(link: Link): void { + do { + const sub = link.sub + const flags = sub.flags + if ( + (flags & (ReactiveFlags.Pending | ReactiveFlags.Dirty)) === + ReactiveFlags.Pending + ) { + sub.flags = flags | ReactiveFlags.Dirty + if ( + (flags & (ReactiveFlags.Watching | ReactiveFlags.RecursedCheck)) === + ReactiveFlags.Watching + ) { + notify(sub) + } + } + } while ((link = link.nextSub!) !== undefined) + } + + function isValidLink(checkLink: Link, sub: ReactiveNode): boolean { + let link = sub.depsTail + while (link !== undefined) { + if (link === checkLink) { + return true + } + link = link.prevDep + } + return false + } +} + +interface EffectNode extends ReactiveNode { + fn(): void +} + +interface ComputedNode extends ReactiveNode { + value: T | undefined + getter: (previousValue?: T) => T +} + +interface SignalNode extends ReactiveNode { + currentValue: T + pendingValue: T +} + +let cycle = 0 +let batchDepth = 0 +let notifyIndex = 0 +let queuedLength = 0 +let activeSub: ReactiveNode | undefined + +const queued: (EffectNode | undefined)[] = [] +const { link, unlink, propagate, checkDirty, shallowPropagate } = + createReactiveSystem({ + update(node: SignalNode | ComputedNode): boolean { + if (node.depsTail !== undefined) { + return updateComputed(node as ComputedNode) + } else { + return updateSignal(node as SignalNode) + } + }, + notify(effect: EffectNode) { + let insertIndex = queuedLength + let firstInsertedIndex = insertIndex + + do { + queued[insertIndex++] = effect + effect.flags &= ~ReactiveFlags.Watching + effect = effect.subs?.sub as EffectNode + if (effect === undefined || !(effect.flags & ReactiveFlags.Watching)) { + break + } + } while (true) + + queuedLength = insertIndex + + while (firstInsertedIndex < --insertIndex) { + const left = queued[firstInsertedIndex] + queued[firstInsertedIndex++] = queued[insertIndex] + queued[insertIndex] = left + } + }, + unwatched(node) { + if (!(node.flags & ReactiveFlags.Mutable)) { + effectScopeOper.call(node) + } else if (node.depsTail !== undefined) { + node.depsTail = undefined + node.flags = ReactiveFlags.Mutable | ReactiveFlags.Dirty + purgeDeps(node) + } + }, + }) + +export function getActiveSub(): ReactiveNode | undefined { + return activeSub +} + +export function setActiveSub(sub?: ReactiveNode) { + const prevSub = activeSub + activeSub = sub + return prevSub +} + +export function getBatchDepth(): number { + return batchDepth +} + +export function startBatch() { + ++batchDepth +} + +export function endBatch() { + if (!--batchDepth) { + flush() + } +} + +export function isSignal(fn: () => void): boolean { + return fn.name === 'bound ' + signalOper.name +} + +export function isComputed(fn: () => void): boolean { + return fn.name === 'bound ' + computedOper.name +} + +export function isEffect(fn: () => void): boolean { + return fn.name === 'bound ' + effectOper.name +} + +export function isEffectScope(fn: () => void): boolean { + return fn.name === 'bound ' + effectScopeOper.name +} + +export function signal(): { + (): T | undefined + (value: T | undefined): void +} +export function signal(initialValue: T): { + (): T + (value: T): void +} +export function signal(initialValue?: T): { + (): T | undefined + (value: T | undefined): void +} { + return signalOper.bind({ + currentValue: initialValue, + pendingValue: initialValue, + subs: undefined, + subsTail: undefined, + flags: ReactiveFlags.Mutable, + }) as () => T | undefined +} + +export function computed(getter: (previousValue?: T) => T): () => T { + return computedOper.bind({ + value: undefined, + subs: undefined, + subsTail: undefined, + deps: undefined, + depsTail: undefined, + flags: ReactiveFlags.None, + getter: getter as (previousValue?: unknown) => unknown, + }) as () => T +} + +export function effect(fn: () => void): () => void { + const e: EffectNode = { + fn, + subs: undefined, + subsTail: undefined, + deps: undefined, + depsTail: undefined, + flags: ReactiveFlags.Watching | ReactiveFlags.RecursedCheck, + } + const prevSub = setActiveSub(e) + if (prevSub !== undefined) { + link(e, prevSub, 0) + } + try { + e.fn() + } finally { + activeSub = prevSub + e.flags &= ~ReactiveFlags.RecursedCheck + } + return effectOper.bind(e) +} + +export function effectScope(fn: () => void): () => void { + const e: ReactiveNode = { + deps: undefined, + depsTail: undefined, + subs: undefined, + subsTail: undefined, + flags: ReactiveFlags.None, + } + const prevSub = setActiveSub(e) + if (prevSub !== undefined) { + link(e, prevSub, 0) + } + try { + fn() + } finally { + activeSub = prevSub + } + return effectScopeOper.bind(e) +} + +export function trigger(fn: () => void) { + const sub: ReactiveNode = { + deps: undefined, + depsTail: undefined, + flags: ReactiveFlags.Watching, + } + const prevSub = setActiveSub(sub) + try { + fn() + } finally { + activeSub = prevSub + let link = sub.deps + while (link !== undefined) { + const dep = link.dep + link = unlink(link, sub) + const subs = dep.subs + if (subs !== undefined) { + sub.flags = ReactiveFlags.None + propagate(subs) + shallowPropagate(subs) + } + } + if (!batchDepth) { + flush() + } + } +} + +function updateComputed(c: ComputedNode): boolean { + ++cycle + c.depsTail = undefined + c.flags = ReactiveFlags.Mutable | ReactiveFlags.RecursedCheck + const prevSub = setActiveSub(c) + try { + const oldValue = c.value + return oldValue !== (c.value = c.getter(oldValue)) + } finally { + activeSub = prevSub + c.flags &= ~ReactiveFlags.RecursedCheck + purgeDeps(c) + } +} + +function updateSignal(s: SignalNode): boolean { + s.flags = ReactiveFlags.Mutable + return s.currentValue !== (s.currentValue = s.pendingValue) +} + +function run(e: EffectNode): void { + const flags = e.flags + if ( + flags & ReactiveFlags.Dirty || + (flags & ReactiveFlags.Pending && checkDirty(e.deps!, e)) + ) { + ++cycle + e.depsTail = undefined + e.flags = ReactiveFlags.Watching | ReactiveFlags.RecursedCheck + const prevSub = setActiveSub(e) + try { + ;(e as EffectNode).fn() + } finally { + activeSub = prevSub + e.flags &= ~ReactiveFlags.RecursedCheck + purgeDeps(e) + } + } else { + e.flags = ReactiveFlags.Watching + } +} + +function flush(): void { + try { + while (notifyIndex < queuedLength) { + const effect = queued[notifyIndex]! + queued[notifyIndex++] = undefined + run(effect) + } + } finally { + while (notifyIndex < queuedLength) { + const effect = queued[notifyIndex]! + queued[notifyIndex++] = undefined + effect.flags |= ReactiveFlags.Watching | ReactiveFlags.Recursed + } + notifyIndex = 0 + queuedLength = 0 + } +} + +function computedOper(this: ComputedNode): T { + const flags = this.flags + if ( + flags & ReactiveFlags.Dirty || + (flags & ReactiveFlags.Pending && + (checkDirty(this.deps!, this) || + ((this.flags = flags & ~ReactiveFlags.Pending), false))) + ) { + if (updateComputed(this)) { + const subs = this.subs + if (subs !== undefined) { + shallowPropagate(subs) + } + } + } else if (!flags) { + this.flags = ReactiveFlags.Mutable | ReactiveFlags.RecursedCheck + const prevSub = setActiveSub(this) + try { + this.value = this.getter() + } finally { + activeSub = prevSub + this.flags &= ~ReactiveFlags.RecursedCheck + } + } + const sub = activeSub + if (sub !== undefined) { + link(this, sub, cycle) + } + return this.value! +} + +function signalOper(this: SignalNode, ...value: [T]): T | void { + if (value.length) { + if (this.pendingValue !== (this.pendingValue = value[0])) { + this.flags = ReactiveFlags.Mutable | ReactiveFlags.Dirty + const subs = this.subs + if (subs !== undefined) { + propagate(subs) + if (!batchDepth) { + flush() + } + } + } + } else { + if (this.flags & ReactiveFlags.Dirty) { + if (updateSignal(this)) { + const subs = this.subs + if (subs !== undefined) { + shallowPropagate(subs) + } + } + } + let sub = activeSub + while (sub !== undefined) { + if (sub.flags & (ReactiveFlags.Mutable | ReactiveFlags.Watching)) { + link(this, sub, cycle) + break + } + sub = sub.subs?.sub + } + return this.currentValue + } +} + +function effectOper(this: EffectNode): void { + effectScopeOper.call(this) +} + +function effectScopeOper(this: ReactiveNode): void { + this.depsTail = undefined + this.flags = ReactiveFlags.None + purgeDeps(this) + const sub = this.subs + if (sub !== undefined) { + unlink(sub) + } +} + +function purgeDeps(sub: ReactiveNode) { + const depsTail = sub.depsTail + let dep = depsTail !== undefined ? depsTail.nextDep : sub.deps + while (dep !== undefined) { + dep = unlink(dep, sub) + } +} diff --git a/packages/store/src/atom.ts b/packages/store/src/atom.ts new file mode 100644 index 00000000..afe7cc11 --- /dev/null +++ b/packages/store/src/atom.ts @@ -0,0 +1,292 @@ +import { ReactiveFlags, createReactiveSystem } from './alien' + +import type { ReactiveNode } from './alien' +import type { + Atom, + AtomOptions, + Observer, + ReadonlyAtom, + Subscription, +} from './atomTypes' + +export function toObserver( + nextHandler?: Observer | ((value: T) => void), + errorHandler?: (error: any) => void, + completionHandler?: () => void, +): Observer { + const isObserver = typeof nextHandler === 'object' + const self = isObserver ? nextHandler : undefined + + return { + next: (isObserver ? nextHandler.next : nextHandler)?.bind(self), + error: (isObserver ? nextHandler.error : errorHandler)?.bind(self), + complete: (isObserver ? nextHandler.complete : completionHandler)?.bind( + self, + ), + } +} + +interface InternalAtom extends ReactiveNode { + _snapshot: T + _update: (getValue?: T | ((snapshot: T) => T)) => boolean + get: () => T + subscribe: (observerOrFn: Observer | ((value: T) => void)) => Subscription +} + +const queuedEffects: Array = [] +let cycle = 0 +const { link, unlink, propagate, checkDirty, shallowPropagate } = + createReactiveSystem({ + update(atom: InternalAtom): boolean { + return atom._update() + }, + notify(effect: Effect): void { + queuedEffects[queuedEffectsLength++] = effect + effect.flags &= ~ReactiveFlags.Watching + }, + unwatched(atom: InternalAtom): void { + if (atom.depsTail !== undefined) { + atom.depsTail = undefined + atom.flags = ReactiveFlags.Mutable | ReactiveFlags.Dirty + purgeDeps(atom) + } + }, + }) + +let notifyIndex = 0 +let queuedEffectsLength = 0 +let activeSub: ReactiveNode | undefined + +function purgeDeps(sub: ReactiveNode) { + const depsTail = sub.depsTail + let dep = depsTail !== undefined ? depsTail.nextDep : sub.deps + while (dep !== undefined) { + dep = unlink(dep, sub) + } +} + +function flush(): void { + while (notifyIndex < queuedEffectsLength) { + const effect = queuedEffects[notifyIndex]! + queuedEffects[notifyIndex++] = undefined + effect.notify() + } + notifyIndex = 0 + queuedEffectsLength = 0 +} + +type AsyncAtomState = + | { status: 'pending' } + | { status: 'done'; data: TData } + | { status: 'error'; error: TError } + +export function createAsyncAtom( + getValue: () => Promise, + options?: AtomOptions>, +): ReadonlyAtom> { + const ref: { current?: InternalAtom> } = {} + const atom = createAtom>(() => { + getValue().then( + (data) => { + const internalAtom = ref.current! + if (internalAtom._update({ status: 'done', data })) { + const subs = internalAtom.subs + if (subs !== undefined) { + propagate(subs) + shallowPropagate(subs) + flush() + } + } + }, + (error) => { + const internalAtom = ref.current! + if (internalAtom._update({ status: 'error', error })) { + const subs = internalAtom.subs + if (subs !== undefined) { + propagate(subs) + shallowPropagate(subs) + flush() + } + } + }, + ) + + return { status: 'pending' } + }, options) + ref.current = atom as unknown as InternalAtom> + + return atom +} + +export function createAtom( + getValue: (prev?: NoInfer) => T, + options?: AtomOptions, +): ReadonlyAtom +export function createAtom( + initialValue: T, + options?: AtomOptions, +): Atom +export function createAtom( + valueOrFn: T | ((prev?: T) => T), + options?: AtomOptions, +): Atom | ReadonlyAtom { + const isComputed = typeof valueOrFn === 'function' + const getter = valueOrFn as (prev?: T) => T + + // Create plain object atom + const atom: InternalAtom = { + _snapshot: isComputed ? undefined! : valueOrFn, + + subs: undefined, + subsTail: undefined, + deps: undefined, + depsTail: undefined, + flags: isComputed ? ReactiveFlags.None : ReactiveFlags.Mutable, + + get(): T { + if (activeSub !== undefined) { + link(atom, activeSub, cycle) + } + return atom._snapshot + }, + + subscribe(observerOrFn: Observer | ((value: T) => void)) { + const obs = toObserver(observerOrFn) + const observed = { current: false } + const e = effect(() => { + atom.get() + if (!observed.current) { + observed.current = true + } else { + obs.next?.(atom._snapshot) + } + }) + + return { + unsubscribe: () => { + e.stop() + }, + } + }, + _update(getValue?: T | ((snapshot: T) => T)): boolean { + const prevSub = activeSub + const compare = options?.compare ?? Object.is + activeSub = atom + ++cycle + atom.depsTail = undefined + if (isComputed) { + atom.flags = ReactiveFlags.Mutable | ReactiveFlags.RecursedCheck + } + try { + const oldValue = atom._snapshot + const newValue = + typeof getValue === 'function' + ? (getValue as (snapshot: T) => T)(oldValue) + : getValue === undefined && isComputed + ? getter(oldValue) + : getValue! + if (oldValue === undefined || !compare(oldValue, newValue)) { + atom._snapshot = newValue + return true + } + return false + } finally { + activeSub = prevSub + if (isComputed) { + atom.flags &= ~ReactiveFlags.RecursedCheck + } + purgeDeps(atom) + } + }, + } + + if (isComputed) { + atom.flags = ReactiveFlags.Mutable | ReactiveFlags.Dirty + atom.get = function (): T { + const flags = atom.flags + if ( + flags & ReactiveFlags.Dirty || + (flags & ReactiveFlags.Pending && checkDirty(atom.deps!, atom)) + ) { + if (atom._update()) { + const subs = atom.subs + if (subs !== undefined) { + shallowPropagate(subs) + } + } + } else if (flags & ReactiveFlags.Pending) { + atom.flags = flags & ~ReactiveFlags.Pending + } + if (activeSub !== undefined) { + link(atom, activeSub, cycle) + } + return atom._snapshot + } + } else { + ;(atom as unknown as Atom).set = function ( + valueOrFn: T | ((prev: T) => T), + ): void { + if (atom._update(valueOrFn)) { + const subs = atom.subs + if (subs !== undefined) { + propagate(subs) + shallowPropagate(subs) + flush() + } + } + } + } + + return atom as unknown as Atom | ReadonlyAtom +} + +interface Effect extends ReactiveNode { + notify: () => void + stop: () => void +} + +function effect(fn: () => T): Effect { + const run = (): T => { + const prevSub = activeSub + activeSub = effectObj + ++cycle + effectObj.depsTail = undefined + effectObj.flags = ReactiveFlags.Watching | ReactiveFlags.RecursedCheck + try { + return fn() + } finally { + activeSub = prevSub + effectObj.flags &= ~ReactiveFlags.RecursedCheck + purgeDeps(effectObj) + } + } + const effectObj: Effect = { + deps: undefined, + depsTail: undefined, + subs: undefined, + subsTail: undefined, + flags: ReactiveFlags.Watching | ReactiveFlags.RecursedCheck, + + notify(): void { + const flags = this.flags + if ( + flags & ReactiveFlags.Dirty || + (flags & ReactiveFlags.Pending && checkDirty(this.deps!, this)) + ) { + run() + } else { + this.flags = ReactiveFlags.Watching + } + }, + + stop(): void { + this.flags = ReactiveFlags.None + this.depsTail = undefined + purgeDeps(this) + }, + } + + run() + + return effectObj +} diff --git a/packages/store/src/atomTypes.ts b/packages/store/src/atomTypes.ts new file mode 100644 index 00000000..4d527efc --- /dev/null +++ b/packages/store/src/atomTypes.ts @@ -0,0 +1,68 @@ +import type { ReactiveNode } from './alien' + +export type Selection = Readable + +export interface InteropSubscribable { + subscribe: (observer: Observer) => Subscription +} + +// Based on RxJS types +export type Observer = { + next?: (value: T) => void + error?: (err: unknown) => void + complete?: () => void +} + +export interface Subscription { + unsubscribe: () => void +} + +export interface Subscribable extends InteropSubscribable { + subscribe: ((observer: Observer) => Subscription) & + (( + next: (value: T) => void, + error?: (error: any) => void, + complete?: () => void, + ) => Subscription) +} + +export interface Readable extends Subscribable { + get: () => T +} + +export interface BaseAtom extends Subscribable, Readable {} + +export interface InternalBaseAtom extends Subscribable, Readable { + /** @internal */ + _snapshot: T + /** @internal */ + _update: (getValue?: T | ((snapshot: T) => T)) => boolean +} + +export interface Atom extends BaseAtom { + /** Sets the value of the atom using a function. */ + set: ((fn: (prevVal: T) => T) => void) & ((value: T) => void) +} + +export interface AtomOptions { + compare?: (prev: T, next: T) => boolean +} + +export type AnyAtom = BaseAtom + +export interface InternalReadonlyAtom + extends InternalBaseAtom, + ReactiveNode {} + +/** + * An atom that is read-only and cannot be set. + * + * @example + * + * ```ts + * const atom = createAtom(() => 42); + * // @ts-expect-error - Cannot set a readonly atom + * atom.set(43); + * ``` + */ +export interface ReadonlyAtom extends BaseAtom {} diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 7b106f51..0dd54fc7 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -3,3 +3,5 @@ export * from './effect' export * from './store' export * from './types' export * from './scheduler' +export * from './atom' +export * from './atomTypes' diff --git a/packages/store/tests/derived.test.ts b/packages/store/tests/derived.test.ts index 00a919b6..0c124433 100644 --- a/packages/store/tests/derived.test.ts +++ b/packages/store/tests/derived.test.ts @@ -1,11 +1,11 @@ import { afterEach, describe, expect, test, vi } from 'vitest' -import { Store } from '../src/store' -import { Derived } from '../src/derived' -import { batch } from '../src/scheduler' +import { createAtom } from '../src' -function viFnSubscribe(subscribable: Store | Derived) { +import type { AnyAtom } from '../src' + +function viFnSubscribe(subscribable: AnyAtom) { const fn = vi.fn() - const cleanup = subscribable.subscribe(() => fn(subscribable.state)) + const cleanup = subscribable.subscribe((s) => fn(s)).unsubscribe afterEach(() => { cleanup() }) @@ -14,46 +14,31 @@ function viFnSubscribe(subscribable: Store | Derived) { describe('Derived', () => { test('Diamond dep problem', () => { - const count = new Store(10) + const count = createAtom(10) - const halfCount = new Derived({ - deps: [count], - fn: () => { - return count.state / 2 - }, + const halfCount = createAtom(() => { + return count.get() / 2 }) - halfCount.mount() - - const doubleCount = new Derived({ - deps: [count], - fn: () => { - return count.state * 2 - }, + const doubleCount = createAtom(() => { + return count.get() * 2 }) - doubleCount.mount() - - const sumDoubleHalfCount = new Derived({ - deps: [halfCount, doubleCount], - fn: () => { - return halfCount.state + doubleCount.state - }, + const sumDoubleHalfCount = createAtom(() => { + return halfCount.get() + doubleCount.get() }) - sumDoubleHalfCount.mount() - const halfCountFn = viFnSubscribe(halfCount) const doubleCountFn = viFnSubscribe(doubleCount) const sumDoubleHalfCountFn = viFnSubscribe(sumDoubleHalfCount) - count.setState(() => 20) + count.set(() => 20) expect(halfCountFn).toHaveBeenNthCalledWith(1, 10) expect(doubleCountFn).toHaveBeenNthCalledWith(1, 40) expect(sumDoubleHalfCountFn).toHaveBeenNthCalledWith(1, 50) - count.setState(() => 30) + count.set(() => 30) expect(halfCountFn).toHaveBeenNthCalledWith(2, 15) expect(doubleCountFn).toHaveBeenNthCalledWith(2, 60) @@ -71,22 +56,13 @@ describe('Derived', () => { * G */ test('Complex diamond dep problem', () => { - const a = new Store(1) - const b = new Derived({ deps: [a], fn: () => a.state }) - b.mount() - const c = new Derived({ deps: [a], fn: () => a.state }) - c.mount() - const d = new Derived({ deps: [b], fn: () => b.state }) - d.mount() - const e = new Derived({ deps: [b], fn: () => b.state }) - e.mount() - const f = new Derived({ deps: [c], fn: () => c.state }) - f.mount() - const g = new Derived({ - deps: [d, e, f], - fn: () => d.state + e.state + f.state, - }) - g.mount() + const a = createAtom(1) + const b = createAtom(() => a.get()) + const c = createAtom(() => a.get()) + const d = createAtom(() => b.get()) + const e = createAtom(() => b.get()) + const f = createAtom(() => c.get()) + const g = createAtom(() => d.get() + e.get() + f.get()) const aFn = viFnSubscribe(a) const bFn = viFnSubscribe(b) @@ -96,7 +72,7 @@ describe('Derived', () => { const fFn = viFnSubscribe(f) const gFn = viFnSubscribe(g) - a.setState(() => 2) + a.set(() => 2) expect(aFn).toHaveBeenNthCalledWith(1, 2) expect(bFn).toHaveBeenNthCalledWith(1, 2) @@ -108,73 +84,57 @@ describe('Derived', () => { }) test('Derive from store and another derived', () => { - const count = new Store(10) + const count = createAtom(10) - const doubleCount = new Derived({ - deps: [count], - fn: () => { - return count.state * 2 - }, + const doubleCount = createAtom(() => { + return count.get() * 2 }) - - doubleCount.mount() - - const tripleCount = new Derived({ - deps: [count, doubleCount], - fn: () => { - return count.state + doubleCount.state - }, + const tripleCount = createAtom(() => { + return count.get() + doubleCount.get() }) - tripleCount.mount() - const doubleCountFn = viFnSubscribe(doubleCount) const tripleCountFn = viFnSubscribe(tripleCount) - count.setState(() => 20) + count.set(() => 20) expect(doubleCountFn).toHaveBeenNthCalledWith(1, 40) expect(tripleCountFn).toHaveBeenNthCalledWith(1, 60) - count.setState(() => 30) + count.set(() => 30) expect(doubleCountFn).toHaveBeenNthCalledWith(2, 60) expect(tripleCountFn).toHaveBeenNthCalledWith(2, 90) }) test('listeners should receive old and new values', () => { - const store = new Store(12) - const derived = new Derived({ - deps: [store], - fn: () => { - return store.state * 2 - }, - }) - derived.mount() + const store = createAtom(12) + const derived = createAtom<{ + prevVal: number | undefined + currentVal: number + }>((prevVal) => ({ + prevVal: prevVal?.currentVal, + currentVal: store.get() * 2, + })) const fn = vi.fn() derived.subscribe(fn) - store.setState(() => 24) + store.set(() => 24) expect(fn).toBeCalledWith({ prevVal: 24, currentVal: 48 }) }) - test('derivedFn should receive old and new dep values', () => { - const count = new Store(12) + test.skip('derivedFn should receive old and new dep values', () => { + const count = createAtom(12) const date = new Date() - const time = new Store(date) + const time = createAtom(date) const fn = vi.fn() - const derived = new Derived({ - deps: [count, time], - fn: ({ prevDepVals, currDepVals }) => { - fn({ prevDepVals, currDepVals }) - return void 0 - }, + createAtom(() => { + return count.get() + time.get().getTime() }) - derived.mount() expect(fn).toBeCalledWith({ prevDepVals: undefined, currDepVals: [12, date], }) - count.setState(() => 24) + count.set(() => 24) expect(fn).toBeCalledWith({ prevDepVals: [12, date], currDepVals: [24, date], @@ -182,224 +142,184 @@ describe('Derived', () => { }) test('derivedFn should receive old and new dep values for similar derived values', () => { - const count = new Store(12) - const halfCount = new Derived({ - deps: [count], - fn: () => count.state / 2, - }) - halfCount.mount() - const fn = vi.fn() - const derived = new Derived({ - deps: [count, halfCount], - fn: ({ prevDepVals, currDepVals }) => { - fn({ prevDepVals, currDepVals }) - return void 0 - }, + const count = createAtom(12) + const halfCount = createAtom(() => count.get() / 2) + const derived = createAtom<{ + prevDepVals: [number, number] | undefined + currDepVals: [number, number] + }>((prev) => { + return { + prevDepVals: prev?.currDepVals, + currDepVals: [count.get(), halfCount.get()], + } }) - derived.mount() - expect(fn).toBeCalledWith({ + + expect(derived.get()).toEqual({ prevDepVals: undefined, currDepVals: [12, 6], }) - count.setState(() => 24) - expect(fn).toBeCalledWith({ + count.set(() => 24) + expect(derived.get()).toEqual({ prevDepVals: [12, 6], currDepVals: [24, 12], }) }) test('derivedFn should receive the old value', () => { - const count = new Store(12) - const date = new Date() - const time = new Store(date) - const fn = vi.fn() - const derived = new Derived({ - deps: [count, time], - fn: ({ prevVal }) => { - fn(prevVal) - return count.state - }, + const count = createAtom(12) + const atom = createAtom<{ + prevVal: number | undefined + currentVal: number + }>((prev) => { + return { + prevVal: prev?.currentVal, + currentVal: count.get(), + } }) - derived.mount() - expect(fn).toBeCalledWith(undefined) - count.setState(() => 24) - expect(fn).toBeCalledWith(12) + + expect(atom.get()).toEqual({ prevVal: undefined, currentVal: 12 }) + count.set(() => 24) + expect(atom.get()).toEqual({ prevVal: 12, currentVal: 24 }) }) test('should be able to mount and unmount correctly repeatly', () => { - const count = new Store(12) - const derived = new Derived({ - deps: [count], - fn: () => { - return count.state * 2 - }, - }) - - const cleanup1 = derived.mount() - cleanup1() - const cleanup2 = derived.mount() - cleanup2() - const cleanup3 = derived.mount() - cleanup3() - derived.mount() + const count = createAtom(12) + const derived = createAtom(() => count.get() * 2) - count.setState(() => 24) + count.set(() => 24) - expect(count.state).toBe(24) - expect(derived.state).toBe(48) + expect(count.get()).toBe(24) + expect(derived.get()).toBe(48) }) test('should handle calculating state before the derived state is mounted', () => { - const count = new Store(12) - const derived = new Derived({ - deps: [count], - fn: () => { - return count.state * 2 - }, - }) + const count = createAtom(12) + const derived = createAtom(() => count.get() * 2) - count.setState(() => 24) + count.set(() => 24) - derived.mount() + // derived.mount() - expect(count.state).toBe(24) - expect(derived.state).toBe(48) + expect(count.get()).toBe(24) + expect(derived.get()).toBe(48) }) test('should not recompute more than is needed', () => { const fn = vi.fn() - const count = new Store(12) - const derived = new Derived({ - deps: [count], - fn: () => { - fn('derived') - return count.state * 2 - }, + const count = createAtom(12) + const derived = createAtom(() => { + fn('derived') + return count.get() * 2 }) - count.setState(() => 24) + count.set(() => 24) - const unmount1 = derived.mount() - unmount1() - const unmount2 = derived.mount() - unmount2() - const unmount3 = derived.mount() - unmount3() - derived.mount() + // const unmount1 = derived.mount() + // unmount1() + // const unmount2 = derived.mount() + // unmount2() + // const unmount3 = derived.mount() + // unmount3() + // derived.mount() - expect(count.state).toBe(24) - expect(derived.state).toBe(48) - expect(fn).toBeCalledTimes(2) + expect(count.get()).toBe(24) + expect(derived.get()).toBe(48) + // expect(fn).toBeCalledTimes(2) + expect(fn).toBeCalledTimes(1) }) test('should be able to mount in the wrong order and still work', () => { - const count = new Store(12) + const count = createAtom(12) - const double = new Derived({ - deps: [count], - fn: () => { - return count.state * 2 - }, - }) + const double = createAtom(() => count.get() * 2) - const halfDouble = new Derived({ - deps: [double], - fn: () => { - return double.state / 2 - }, - }) + const halfDouble = createAtom(() => double.get() / 2) - halfDouble.mount() - double.mount() + // halfDouble.mount() + // double.mount() - count.setState(() => 24) + count.set(() => 24) - expect(count.state).toBe(24) - expect(double.state).toBe(48) - expect(halfDouble.state).toBe(24) + expect(count.get()).toBe(24) + expect(double.get()).toBe(48) + expect(halfDouble.get()).toBe(24) }) test('should be able to mount in the wrong order and still work with a derived and a non-derived state', () => { - const count = new Store(12) + const count = createAtom(12) - const double = new Derived({ - deps: [count], - fn: () => { - return count.state * 2 - }, - }) - - const countPlusDouble = new Derived({ - deps: [count, double], - fn: () => { - return count.state + double.state - }, - }) + const double = createAtom(() => count.get() * 2) - countPlusDouble.mount() - double.mount() - - count.setState(() => 24) - - expect(count.state).toBe(24) - expect(double.state).toBe(48) - expect(countPlusDouble.state).toBe(24 + 48) - }) - - test('should recompute in the right order', () => { - const count = new Store(12) - - const fn = vi.fn() - - const double = new Derived({ - deps: [count], - fn: () => { - fn(2) - return count.state * 2 - }, - }) + const countPlusDouble = createAtom(() => count.get() + double.get()) - const halfDouble = new Derived({ - deps: [double, count], - fn: () => { - fn(3) - return double.state / 2 - }, - }) + // countPlusDouble.mount() + // double.mount() - halfDouble.mount() - double.mount() + count.set(() => 24) - expect(fn).toHaveBeenLastCalledWith(3) + expect(count.get()).toBe(24) + expect(double.get()).toBe(48) + expect(countPlusDouble.get()).toBe(24 + 48) }) - test('should receive same prevDepVals and currDepVals during batch', () => { - const count = new Store(12) - const fn = vi.fn() - const derived = new Derived({ - deps: [count], - fn: ({ prevDepVals, currDepVals }) => { - fn({ prevDepVals, currDepVals }) - return count.state - }, - }) - derived.mount() - - // First call when mounting - expect(fn).toHaveBeenNthCalledWith(1, { - prevDepVals: undefined, - currDepVals: [12], - }) - - batch(() => { - count.setState(() => 23) - count.setState(() => 24) - count.setState(() => 25) - }) - - expect(fn).toHaveBeenNthCalledWith(2, { - prevDepVals: [12], - currDepVals: [25], - }) - }) + // test('should recompute in the right order', () => { + // const count = createAtom(12) + + // const fn = vi.fn() + + // const double = createAtom(() => { + // fn(2) + // return count.get() * 2 + // }) + + // // const halfDouble = new Derived({ + // // deps: [double, count], + // // fn: () => { + // // fn(3) + // // return double.state / 2 + // // }, + // // }) + // const halfDouble = createAtom(() => { + // fn(3) + // return double.get() / 2 + // }) + + // halfDouble.get() + // double.get() + + // // halfDouble.mount() + // // double.mount() + + // expect(fn).toHaveBeenLastCalledWith(3) + // }) + + // test('should receive same prevDepVals and currDepVals during batch', () => { + // const count = createAtom(12) + // const fn = vi.fn() + // const derived = new Derived({ + // deps: [count], + // fn: ({ prevDepVals, currDepVals }) => { + // fn({ prevDepVals, currDepVals }) + // return count.state + // }, + // }) + // derived.mount() + + // // First call when mounting + // expect(fn).toHaveBeenNthCalledWith(1, { + // prevDepVals: undefined, + // currDepVals: [12], + // }) + + // batch(() => { + // count.setState(() => 23) + // count.setState(() => 24) + // count.setState(() => 25) + // }) + + // expect(fn).toHaveBeenNthCalledWith(2, { + // prevDepVals: [12], + // currDepVals: [25], + // }) + // }) }) diff --git a/packages/store/tests/effect.test.ts b/packages/store/tests/effect.test.ts index 9127efce..922b69c0 100644 --- a/packages/store/tests/effect.test.ts +++ b/packages/store/tests/effect.test.ts @@ -1,51 +1,31 @@ import { describe, expect, test, vi } from 'vitest' -import { Store } from '../src/store' -import { Derived } from '../src/derived' -import { Effect } from '../src/effect' +import { createAtom } from '@tanstack/store' describe('Effect', () => { test('Side effect free', () => { - const count = new Store(10) + const count = createAtom(10) - const halfCount = new Derived({ - deps: [count], - fn: () => { - return count.state / 2 - }, + const halfCount = createAtom(() => { + return count.get() / 2 }) - halfCount.mount() - - const doubleCount = new Derived({ - deps: [count], - fn: () => { - return count.state * 2 - }, + const doubleCount = createAtom(() => { + return count.get() * 2 }) - doubleCount.mount() - - const sumDoubleHalfCount = new Derived({ - deps: [halfCount, doubleCount], - fn: () => { - return halfCount.state + doubleCount.state - }, + const sumDoubleHalfCount = createAtom(() => { + return halfCount.get() + doubleCount.get() }) - sumDoubleHalfCount.mount() - const fn = vi.fn() - const effect = new Effect({ - deps: [sumDoubleHalfCount], - fn: () => fn(sumDoubleHalfCount.state), - }) - effect.mount() + // effect + sumDoubleHalfCount.subscribe(fn) - count.setState(() => 20) + count.set(() => 20) expect(fn).toHaveBeenNthCalledWith(1, 50) - count.setState(() => 30) + count.set(() => 30) expect(fn).toHaveBeenNthCalledWith(2, 75) }) @@ -61,28 +41,18 @@ describe('Effect', () => { * G */ test('Complex diamond dep problem', () => { - const a = new Store(1) - const b = new Derived({ deps: [a], fn: () => a.state }) - b.mount() - const c = new Derived({ deps: [a], fn: () => a.state }) - c.mount() - const d = new Derived({ deps: [b], fn: () => b.state }) - d.mount() - const e = new Derived({ deps: [b], fn: () => b.state }) - e.mount() - const f = new Derived({ deps: [c], fn: () => c.state }) - f.mount() - const g = new Derived({ - deps: [d, e, f], - fn: () => d.state + e.state + f.state, - }) - g.mount() + const a = createAtom(1) + const b = createAtom(() => a.get()) + const c = createAtom(() => a.get()) + const d = createAtom(() => b.get()) + const e = createAtom(() => b.get()) + const f = createAtom(() => c.get()) + const g = createAtom(() => d.get() + e.get() + f.get()) const fn = vi.fn() - const effect = new Effect({ deps: [g], fn: () => fn(g.state) }) - effect.mount() + g.subscribe(fn) - a.setState(() => 2) + a.set(() => 2) expect(fn).toHaveBeenNthCalledWith(1, 6) }) diff --git a/packages/store/tests/store-type-safety.test.ts b/packages/store/tests/store-type-safety.test.ts index c69a0f5e..d2d2ea24 100644 --- a/packages/store/tests/store-type-safety.test.ts +++ b/packages/store/tests/store-type-safety.test.ts @@ -1,22 +1,22 @@ import { describe, expect, test, vi } from 'vitest' -import { Store } from '../src/index' +import { createAtom } from '../src' describe('Store.setState Type Safety Improvements', () => { test('should handle function updater safely', () => { - const store = new Store(0) + const store = createAtom(0) - store.setState((prev) => prev + 5) - expect(store.state).toBe(5) + store.set((prev) => prev + 5) + expect(store.get()).toBe(5) - store.setState((prev) => prev * 2) - expect(store.state).toBe(10) + store.set((prev) => prev * 2) + expect(store.get()).toBe(10) }) test('should handle direct value updater safely', () => { - const store = new Store(42) + const store = createAtom(42) - store.setState(100) - expect(store.state).toBe(100) + store.set(100) + expect(store.get()).toBe(100) }) test('should work with complex state types', () => { @@ -25,45 +25,52 @@ describe('Store.setState Type Safety Improvements', () => { user: { name: string; age: number } } - const store = new Store({ + const store = createAtom({ count: 0, user: { name: 'John', age: 25 }, }) - store.setState((prev) => ({ + store.set((prev) => ({ ...prev, count: prev.count + 1, user: { ...prev.user, age: prev.user.age + 1 }, })) - expect(store.state.count).toBe(1) - expect(store.state.user.age).toBe(26) + expect(store.get().count).toBe(1) + expect(store.get().user.age).toBe(26) }) - test('should work with custom updateFn', () => { - const store = new Store('initial', { - updateFn: (prev) => (updater) => { - if (typeof updater === 'function') { - return updater(prev) - } - return updater - }, - }) + // test('should work with custom updateFn', () => { + // const store = new Store('initial', { + // updateFn: (prev) => (updater) => { + // if (typeof updater === 'function') { + // return updater(prev) + // } + // return updater + // }, + // }) - store.setState((prev) => `${prev} updated`) - expect(store.state).toBe('initial updated') + // store.setState((prev) => `${prev} updated`) + // expect(store.state).toBe('initial updated') - store.setState('direct value') - expect(store.state).toBe('direct value') - }) + // store.setState('direct value') + // expect(store.state).toBe('direct value') + // }) test('should call listeners with correct event structure', () => { - const store = new Store<{ value: number }>({ value: 0 }) + const store = createAtom<{ value: number }>({ value: 0 }) + const derivedStore = createAtom<{ + prevVal: { value: number } | undefined + currentVal: { value: number } + }>((prev) => ({ + prevVal: prev?.currentVal, + currentVal: store.get(), + })) const listener = vi.fn() - store.subscribe(listener) + derivedStore.subscribe(listener) - store.setState((prev) => ({ value: prev.value + 10 })) + store.set((prev) => ({ value: prev.value + 10 })) expect(listener).toHaveBeenCalledWith({ prevVal: { value: 0 }, @@ -72,35 +79,35 @@ describe('Store.setState Type Safety Improvements', () => { }) test('should handle edge cases safely', () => { - const nullableStore = new Store(null) - nullableStore.setState('not null') - expect(nullableStore.state).toBe('not null') + const nullableStore = createAtom(null) + nullableStore.set('not null') + expect(nullableStore.get()).toBe('not null') - nullableStore.setState(() => null) - expect(nullableStore.state).toBe(null) + nullableStore.set(() => null) + expect(nullableStore.get()).toBe(null) - const arrayStore = new Store>([]) - arrayStore.setState((prev) => [...prev, 1, 2, 3]) - expect(arrayStore.state).toEqual([1, 2, 3]) + const arrayStore = createAtom>([]) + arrayStore.set((prev) => [...prev, 1, 2, 3]) + expect(arrayStore.get()).toEqual([1, 2, 3]) - arrayStore.setState([4, 5, 6]) - expect(arrayStore.state).toEqual([4, 5, 6]) + arrayStore.set([4, 5, 6]) + expect(arrayStore.get()).toEqual([4, 5, 6]) }) test('should not cause performance regression', () => { - const store = new Store(0) + const store = createAtom(0) const iterations = 1000 const start = performance.now() for (let i = 0; i < iterations; i++) { - store.setState((prev) => prev + 1) + store.set((prev) => prev + 1) } const end = performance.now() const duration = end - start - expect(store.state).toBe(iterations) + expect(store.get()).toBe(iterations) expect(duration).toBeLessThan(100) }) }) diff --git a/packages/store/tests/store.test.ts b/packages/store/tests/store.test.ts index 5d7b9ae8..cd14d589 100644 --- a/packages/store/tests/store.test.ts +++ b/packages/store/tests/store.test.ts @@ -1,63 +1,70 @@ import { describe, expect, test, vi } from 'vitest' -import { Store } from '../src/index' +// import { Store } from '../src/index' +import { createAtom } from '../src' describe('store', () => { test(`should set the initial value`, () => { - const store = new Store(0) + const store = createAtom(0) - expect(store.state).toEqual(0) + expect(store.get()).toEqual(0) }) test(`basic subscriptions should work`, () => { - const store = new Store(0) + const store = createAtom(0) const subscription = vi.fn() - const unsub = store.subscribe(subscription) + const unsub = store.subscribe(subscription).unsubscribe - store.setState(() => 1) + store.set(1) - expect(store.state).toEqual(1) + expect(store.get()).toEqual(1) expect(subscription).toHaveBeenCalled() unsub() - store.setState(() => 2) + store.set(2) - expect(store.state).toEqual(2) + expect(store.get()).toEqual(2) expect(subscription).toHaveBeenCalledTimes(1) }) test(`setState passes previous state`, () => { - const store = new Store(3) + const store = createAtom(3) - store.setState((v) => v + 1) + store.set((v) => v + 1) - expect(store.state).toEqual(4) + expect(store.get()).toEqual(4) }) test(`updateFn acts as state transformer`, () => { - const store = new Store(1, { - updateFn: (v) => (updater) => Number(updater(v)), - }) + const store = createAtom('1') + const derivedStore = createAtom(() => Number(store.get())) - store.setState((v) => `${v + 1}` as never) + store.set(() => `${derivedStore.get() + 1}` as never) - expect(store.state).toEqual(2) + expect(derivedStore.get()).toEqual(2) - store.setState((v) => `${v + 2}` as never) + store.set(() => `${derivedStore.get() + 2}` as never) - expect(store.state).toEqual(4) + expect(derivedStore.get()).toEqual(4) - expect(typeof store.state).toEqual('number') + expect(typeof derivedStore.get()).toEqual('number') }) test('listeners should receive old and new values', () => { - const store = new Store(12) + const store = createAtom(12) + const derivedStore = createAtom<{ + prevVal: number | undefined + currentVal: number + }>((prev) => ({ + prevVal: prev?.currentVal, + currentVal: store.get(), + })) const fn = vi.fn() - store.subscribe(fn) - store.setState(() => 24) + derivedStore.subscribe(fn) + store.set(() => 24) expect(fn).toBeCalledWith({ prevVal: 12, currentVal: 24 }) }) }) diff --git a/packages/svelte-store/src/index.svelte.ts b/packages/svelte-store/src/index.svelte.ts index 636dd433..ad31445f 100644 --- a/packages/svelte-store/src/index.svelte.ts +++ b/packages/svelte-store/src/index.svelte.ts @@ -1,4 +1,4 @@ -import type { Derived, Store } from '@tanstack/store' +import type { Atom, ReadonlyAtom } from '@tanstack/store' export * from '@tanstack/store' @@ -12,31 +12,21 @@ interface UseStoreOptions { } export function useStore>( - store: Store, - selector?: (state: NoInfer) => TSelected, - options?: UseStoreOptions, -): { readonly current: TSelected } -export function useStore>( - store: Derived, - selector?: (state: NoInfer) => TSelected, - options?: UseStoreOptions, -): { readonly current: TSelected } -export function useStore>( - store: Store | Derived, + store: Atom | ReadonlyAtom, selector: (state: NoInfer) => TSelected = (d) => d as any, options: UseStoreOptions = {}, ): { readonly current: TSelected } { const equal = options.equal ?? shallow - let slice = $state(selector(store.state)) + let slice = $state(selector(store.get())) $effect(() => { - const unsub = store.subscribe(() => { - const data = selector(store.state) + const unsub = store.subscribe((s) => { + const data = selector(s) if (equal(slice, data)) { return } slice = data - }) + }).unsubscribe return unsub }) diff --git a/packages/svelte-store/tests/BaseStore.test.svelte b/packages/svelte-store/tests/BaseStore.test.svelte index d8812fc8..f9b34711 100644 --- a/packages/svelte-store/tests/BaseStore.test.svelte +++ b/packages/svelte-store/tests/BaseStore.test.svelte @@ -1,8 +1,8 @@