diff --git a/docs/framework/react/reference/functions/shallow.md b/docs/framework/react/reference/functions/shallow.md index 241360d..ee9ef99 100644 --- a/docs/framework/react/reference/functions/shallow.md +++ b/docs/framework/react/reference/functions/shallow.md @@ -11,7 +11,7 @@ title: shallow function shallow(objA, objB): boolean ``` -Defined in: [index.ts:34](https://github.com/TanStack/store/blob/main/packages/react-store/src/index.ts#L34) +Defined in: [index.ts:42](https://github.com/TanStack/store/blob/main/packages/react-store/src/index.ts#L42) ## Type Parameters diff --git a/docs/framework/react/reference/functions/usestore.md b/docs/framework/react/reference/functions/usestore.md index 1b1ab55..c35c203 100644 --- a/docs/framework/react/reference/functions/usestore.md +++ b/docs/framework/react/reference/functions/usestore.md @@ -10,10 +10,13 @@ title: useStore ## Call Signature ```ts -function useStore(store, selector?): TSelected +function useStore( + store, + selector?, + options?): TSelected ``` -Defined in: [index.ts:11](https://github.com/TanStack/store/blob/main/packages/react-store/src/index.ts#L11) +Defined in: [index.ts:15](https://github.com/TanStack/store/blob/main/packages/react-store/src/index.ts#L15) ### Type Parameters @@ -31,6 +34,10 @@ Defined in: [index.ts:11](https://github.com/TanStack/store/blob/main/packages/r (`state`) => `TSelected` +#### options? + +`UseStoreOptions`\<`TSelected`\> + ### Returns `TSelected` @@ -38,10 +45,13 @@ Defined in: [index.ts:11](https://github.com/TanStack/store/blob/main/packages/r ## Call Signature ```ts -function useStore(store, selector?): TSelected +function useStore( + store, + selector?, + options?): TSelected ``` -Defined in: [index.ts:15](https://github.com/TanStack/store/blob/main/packages/react-store/src/index.ts#L15) +Defined in: [index.ts:20](https://github.com/TanStack/store/blob/main/packages/react-store/src/index.ts#L20) ### Type Parameters @@ -59,6 +69,10 @@ Defined in: [index.ts:15](https://github.com/TanStack/store/blob/main/packages/r (`state`) => `TSelected` +#### options? + +`UseStoreOptions`\<`TSelected`\> + ### Returns `TSelected` diff --git a/docs/framework/solid/reference/functions/shallow.md b/docs/framework/solid/reference/functions/shallow.md index bcc1517..44ddcf9 100644 --- a/docs/framework/solid/reference/functions/shallow.md +++ b/docs/framework/solid/reference/functions/shallow.md @@ -11,7 +11,7 @@ title: shallow function shallow(objA, objB): boolean ``` -Defined in: [index.tsx:41](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L41) +Defined in: [index.tsx:49](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L49) ## Type Parameters diff --git a/docs/framework/solid/reference/functions/usestore.md b/docs/framework/solid/reference/functions/usestore.md index 86e4053..fbc1c7a 100644 --- a/docs/framework/solid/reference/functions/usestore.md +++ b/docs/framework/solid/reference/functions/usestore.md @@ -10,10 +10,13 @@ title: useStore ## Call Signature ```ts -function useStore(store, selector?): Accessor +function useStore( + store, + selector?, +options?): Accessor ``` -Defined in: [index.tsx:12](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L12) +Defined in: [index.tsx:16](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L16) ### Type Parameters @@ -31,6 +34,10 @@ Defined in: [index.tsx:12](https://github.com/TanStack/store/blob/main/packages/ (`state`) => `TSelected` +#### options? + +`UseStoreOptions`\<`TSelected`\> + ### Returns `Accessor`\<`TSelected`\> @@ -38,10 +45,13 @@ Defined in: [index.tsx:12](https://github.com/TanStack/store/blob/main/packages/ ## Call Signature ```ts -function useStore(store, selector?): Accessor +function useStore( + store, + selector?, +options?): Accessor ``` -Defined in: [index.tsx:16](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L16) +Defined in: [index.tsx:21](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L21) ### Type Parameters @@ -59,6 +69,10 @@ Defined in: [index.tsx:16](https://github.com/TanStack/store/blob/main/packages/ (`state`) => `TSelected` +#### options? + +`UseStoreOptions`\<`TSelected`\> + ### Returns `Accessor`\<`TSelected`\> diff --git a/docs/framework/svelte/reference/functions/shallow.md b/docs/framework/svelte/reference/functions/shallow.md index 9ceecfe..54c2ad4 100644 --- a/docs/framework/svelte/reference/functions/shallow.md +++ b/docs/framework/svelte/reference/functions/shallow.md @@ -11,7 +11,7 @@ title: shallow function shallow(objA, objB): boolean ``` -Defined in: [index.svelte.ts:43](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L43) +Defined in: [index.svelte.ts:51](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L51) ## Type Parameters diff --git a/docs/framework/svelte/reference/functions/usestore.md b/docs/framework/svelte/reference/functions/usestore.md index 1783b5e..d713b61 100644 --- a/docs/framework/svelte/reference/functions/usestore.md +++ b/docs/framework/svelte/reference/functions/usestore.md @@ -10,10 +10,13 @@ title: useStore ## Call Signature ```ts -function useStore(store, selector?): object +function useStore( + store, + selector?, + options?): object ``` -Defined in: [index.svelte.ts:10](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L10) +Defined in: [index.svelte.ts:14](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L14) ### Type Parameters @@ -31,6 +34,10 @@ Defined in: [index.svelte.ts:10](https://github.com/TanStack/store/blob/main/pac (`state`) => `TSelected` +#### options? + +`UseStoreOptions`\<`TSelected`\> + ### Returns `object` @@ -44,10 +51,13 @@ readonly current: TSelected; ## Call Signature ```ts -function useStore(store, selector?): object +function useStore( + store, + selector?, + options?): object ``` -Defined in: [index.svelte.ts:14](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L14) +Defined in: [index.svelte.ts:19](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L19) ### Type Parameters @@ -65,6 +75,10 @@ Defined in: [index.svelte.ts:14](https://github.com/TanStack/store/blob/main/pac (`state`) => `TSelected` +#### options? + +`UseStoreOptions`\<`TSelected`\> + ### Returns `object` diff --git a/docs/framework/vue/reference/functions/shallow.md b/docs/framework/vue/reference/functions/shallow.md index 11cc076..632476e 100644 --- a/docs/framework/vue/reference/functions/shallow.md +++ b/docs/framework/vue/reference/functions/shallow.md @@ -11,7 +11,7 @@ title: shallow function shallow(objA, objB): boolean ``` -Defined in: [index.ts:47](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L47) +Defined in: [index.ts:55](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L55) ## Type Parameters diff --git a/docs/framework/vue/reference/functions/usestore.md b/docs/framework/vue/reference/functions/usestore.md index 59c6773..c02aabc 100644 --- a/docs/framework/vue/reference/functions/usestore.md +++ b/docs/framework/vue/reference/functions/usestore.md @@ -10,10 +10,13 @@ title: useStore ## Call Signature ```ts -function useStore(store, selector?): Readonly> +function useStore( + store, + selector?, +options?): Readonly> ``` -Defined in: [index.ts:12](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L12) +Defined in: [index.ts:16](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L16) ### Type Parameters @@ -31,6 +34,10 @@ Defined in: [index.ts:12](https://github.com/TanStack/store/blob/main/packages/v (`state`) => `TSelected` +#### options? + +`UseStoreOptions`\<`TSelected`\> + ### Returns `Readonly`\<`Ref`\<`TSelected`, `TSelected`\>\> @@ -38,10 +45,13 @@ Defined in: [index.ts:12](https://github.com/TanStack/store/blob/main/packages/v ## Call Signature ```ts -function useStore(store, selector?): Readonly> +function useStore( + store, + selector?, +options?): Readonly> ``` -Defined in: [index.ts:16](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L16) +Defined in: [index.ts:21](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L21) ### Type Parameters @@ -59,6 +69,10 @@ Defined in: [index.ts:16](https://github.com/TanStack/store/blob/main/packages/v (`state`) => `TSelected` +#### options? + +`UseStoreOptions`\<`TSelected`\> + ### Returns `Readonly`\<`Ref`\<`TSelected`, `TSelected`\>\> diff --git a/packages/react-store/src/index.ts b/packages/react-store/src/index.ts index 80ccdbc..21d3a47 100644 --- a/packages/react-store/src/index.ts +++ b/packages/react-store/src/index.ts @@ -7,25 +7,33 @@ export * from '@tanstack/store' * @private */ export type NoInfer = [T][T extends any ? 0 : never] +type EqualityFn = (objA: T, objB: T) => boolean +interface UseStoreOptions { + equal?: EqualityFn +} 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, selector: (state: NoInfer) => TSelected = (d) => d as any, + options: UseStoreOptions = {}, ): TSelected { + const equal = options.equal ?? shallow const slice = useSyncExternalStoreWithSelector( store.subscribe, () => store.state, () => store.state, selector, - shallow, + equal, ) return slice diff --git a/packages/react-store/tests/index.test.tsx b/packages/react-store/tests/index.test.tsx index 60bca12..37d1d37 100644 --- a/packages/react-store/tests/index.test.tsx +++ b/packages/react-store/tests/index.test.tsx @@ -78,6 +78,78 @@ describe('useStore', () => { expect(getByText('Number rendered: 2')).toBeInTheDocument() }) + it('allow specifying custom equality function', async () => { + const store = new Store({ + array: [ + { select: 0, ignore: 1 }, + { select: 0, ignore: 1 }, + ], + }) + + function deepEqual(objA: T, objB: T) { + return JSON.stringify(objA) === JSON.stringify(objB) + } + + function Comp() { + const storeVal = useStore( + store, + (state) => state.array.map(({ ignore, ...rest }) => rest), + { equal: deepEqual }, + ) + const [fn] = useState(vi.fn) + fn() + + const value = storeVal + .map((item) => item.select) + .reduce((total, num) => total + num, 0) + + return ( +
+

Number rendered: {fn.mock.calls.length}

+

Store: {value}

+ + +
+ ) + } + + const { getByText } = render() + expect(getByText('Store: 0')).toBeInTheDocument() + expect(getByText('Number rendered: 1')).toBeInTheDocument() + + await user.click(getByText('Update select')) + + await waitFor(() => expect(getByText('Store: 10')).toBeInTheDocument()) + expect(getByText('Number rendered: 2')).toBeInTheDocument() + + await user.click(getByText('Update ignored')) + expect(getByText('Number rendered: 2')).toBeInTheDocument() + }) + it('works with mounted derived stores', async () => { const store = new Store(0) diff --git a/packages/solid-store/src/index.tsx b/packages/solid-store/src/index.tsx index d36ed5c..104653b 100644 --- a/packages/solid-store/src/index.tsx +++ b/packages/solid-store/src/index.tsx @@ -8,24 +8,32 @@ export * from '@tanstack/store' * @private */ export type NoInfer = [T][T extends any ? 0 : never] +type EqualityFn = (objA: T, objB: T) => boolean +interface UseStoreOptions { + equal?: EqualityFn +} 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, selector: (state: NoInfer) => TSelected = (d) => d as any, + options: UseStoreOptions = {}, ): Accessor { const [signal, setSignal] = createSignal(selector(store.state)) + const equal = options.equal ?? shallow const unsub = store.subscribe(() => { const data = selector(store.state) - if (shallow(signal(), data)) { + if (equal(signal(), data)) { return } setSignal(() => data) diff --git a/packages/svelte-store/src/index.svelte.ts b/packages/svelte-store/src/index.svelte.ts index 05f3cac..636dd43 100644 --- a/packages/svelte-store/src/index.svelte.ts +++ b/packages/svelte-store/src/index.svelte.ts @@ -6,25 +6,33 @@ export * from '@tanstack/store' * @private */ export type NoInfer = [T][T extends any ? 0 : never] +type EqualityFn = (objA: T, objB: T) => boolean +interface UseStoreOptions { + equal?: EqualityFn +} 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, selector: (state: NoInfer) => TSelected = (d) => d as any, + options: UseStoreOptions = {}, ): { readonly current: TSelected } { + const equal = options.equal ?? shallow let slice = $state(selector(store.state)) $effect(() => { const unsub = store.subscribe(() => { const data = selector(store.state) - if (shallow(slice, data)) { + if (equal(slice, data)) { return } slice = data diff --git a/packages/vue-store/src/index.ts b/packages/vue-store/src/index.ts index afd18e8..6b2ebd0 100644 --- a/packages/vue-store/src/index.ts +++ b/packages/vue-store/src/index.ts @@ -8,27 +8,35 @@ export * from '@tanstack/store' * @private */ export type NoInfer = [T][T extends any ? 0 : never] +type EqualityFn = (objA: T, objB: T) => boolean +interface UseStoreOptions { + equal?: EqualityFn +} export function useStore>( store: Store, selector?: (state: NoInfer) => TSelected, + options?: UseStoreOptions, ): Readonly> export function useStore>( store: Derived, selector?: (state: NoInfer) => TSelected, + options?: UseStoreOptions, ): Readonly> export function useStore>( store: Store | Derived, selector: (state: NoInfer) => TSelected = (d) => d as any, + options: UseStoreOptions = {}, ): Readonly> { const slice = ref(selector(store.state)) as Ref + const equal = options.equal ?? shallow watch( () => store, (value, _oldValue, onCleanup) => { const unsub = value.subscribe(() => { const data = selector(value.state) - if (shallow(toRaw(slice.value), data)) { + if (equal(toRaw(slice.value), data)) { return } slice.value = data