Skip to content

Commit be4f045

Browse files
feat: allow specifying comparator / equality function (#244)
* Add test case for deep nested comparison * Add equality function * Add option for custom equal function everywhere * ci: apply automated fixes and generate docs --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 4a8bdea commit be4f045

File tree

13 files changed

+184
-24
lines changed

13 files changed

+184
-24
lines changed

docs/framework/react/reference/functions/shallow.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ title: shallow
1111
function shallow<T>(objA, objB): boolean
1212
```
1313

14-
Defined in: [index.ts:34](https://github.com/TanStack/store/blob/main/packages/react-store/src/index.ts#L34)
14+
Defined in: [index.ts:42](https://github.com/TanStack/store/blob/main/packages/react-store/src/index.ts#L42)
1515

1616
## Type Parameters
1717

docs/framework/react/reference/functions/usestore.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ title: useStore
1010
## Call Signature
1111

1212
```ts
13-
function useStore<TState, TSelected>(store, selector?): TSelected
13+
function useStore<TState, TSelected>(
14+
store,
15+
selector?,
16+
options?): TSelected
1417
```
1518

16-
Defined in: [index.ts:11](https://github.com/TanStack/store/blob/main/packages/react-store/src/index.ts#L11)
19+
Defined in: [index.ts:15](https://github.com/TanStack/store/blob/main/packages/react-store/src/index.ts#L15)
1720

1821
### Type Parameters
1922

@@ -31,17 +34,24 @@ Defined in: [index.ts:11](https://github.com/TanStack/store/blob/main/packages/r
3134

3235
(`state`) => `TSelected`
3336

37+
#### options?
38+
39+
`UseStoreOptions`\<`TSelected`\>
40+
3441
### Returns
3542

3643
`TSelected`
3744

3845
## Call Signature
3946

4047
```ts
41-
function useStore<TState, TSelected>(store, selector?): TSelected
48+
function useStore<TState, TSelected>(
49+
store,
50+
selector?,
51+
options?): TSelected
4252
```
4353

44-
Defined in: [index.ts:15](https://github.com/TanStack/store/blob/main/packages/react-store/src/index.ts#L15)
54+
Defined in: [index.ts:20](https://github.com/TanStack/store/blob/main/packages/react-store/src/index.ts#L20)
4555

4656
### Type Parameters
4757

@@ -59,6 +69,10 @@ Defined in: [index.ts:15](https://github.com/TanStack/store/blob/main/packages/r
5969

6070
(`state`) => `TSelected`
6171

72+
#### options?
73+
74+
`UseStoreOptions`\<`TSelected`\>
75+
6276
### Returns
6377

6478
`TSelected`

docs/framework/solid/reference/functions/shallow.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ title: shallow
1111
function shallow<T>(objA, objB): boolean
1212
```
1313

14-
Defined in: [index.tsx:41](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L41)
14+
Defined in: [index.tsx:49](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L49)
1515

1616
## Type Parameters
1717

docs/framework/solid/reference/functions/usestore.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ title: useStore
1010
## Call Signature
1111

1212
```ts
13-
function useStore<TState, TSelected>(store, selector?): Accessor<TSelected>
13+
function useStore<TState, TSelected>(
14+
store,
15+
selector?,
16+
options?): Accessor<TSelected>
1417
```
1518

16-
Defined in: [index.tsx:12](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L12)
19+
Defined in: [index.tsx:16](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L16)
1720

1821
### Type Parameters
1922

@@ -31,17 +34,24 @@ Defined in: [index.tsx:12](https://github.com/TanStack/store/blob/main/packages/
3134

3235
(`state`) => `TSelected`
3336

37+
#### options?
38+
39+
`UseStoreOptions`\<`TSelected`\>
40+
3441
### Returns
3542

3643
`Accessor`\<`TSelected`\>
3744

3845
## Call Signature
3946

4047
```ts
41-
function useStore<TState, TSelected>(store, selector?): Accessor<TSelected>
48+
function useStore<TState, TSelected>(
49+
store,
50+
selector?,
51+
options?): Accessor<TSelected>
4252
```
4353

44-
Defined in: [index.tsx:16](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L16)
54+
Defined in: [index.tsx:21](https://github.com/TanStack/store/blob/main/packages/solid-store/src/index.tsx#L21)
4555

4656
### Type Parameters
4757

@@ -59,6 +69,10 @@ Defined in: [index.tsx:16](https://github.com/TanStack/store/blob/main/packages/
5969

6070
(`state`) => `TSelected`
6171

72+
#### options?
73+
74+
`UseStoreOptions`\<`TSelected`\>
75+
6276
### Returns
6377

6478
`Accessor`\<`TSelected`\>

docs/framework/svelte/reference/functions/shallow.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ title: shallow
1111
function shallow<T>(objA, objB): boolean
1212
```
1313

14-
Defined in: [index.svelte.ts:43](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L43)
14+
Defined in: [index.svelte.ts:51](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L51)
1515

1616
## Type Parameters
1717

docs/framework/svelte/reference/functions/usestore.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ title: useStore
1010
## Call Signature
1111

1212
```ts
13-
function useStore<TState, TSelected>(store, selector?): object
13+
function useStore<TState, TSelected>(
14+
store,
15+
selector?,
16+
options?): object
1417
```
1518

16-
Defined in: [index.svelte.ts:10](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L10)
19+
Defined in: [index.svelte.ts:14](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L14)
1720

1821
### Type Parameters
1922

@@ -31,6 +34,10 @@ Defined in: [index.svelte.ts:10](https://github.com/TanStack/store/blob/main/pac
3134

3235
(`state`) => `TSelected`
3336

37+
#### options?
38+
39+
`UseStoreOptions`\<`TSelected`\>
40+
3441
### Returns
3542

3643
`object`
@@ -44,10 +51,13 @@ readonly current: TSelected;
4451
## Call Signature
4552

4653
```ts
47-
function useStore<TState, TSelected>(store, selector?): object
54+
function useStore<TState, TSelected>(
55+
store,
56+
selector?,
57+
options?): object
4858
```
4959

50-
Defined in: [index.svelte.ts:14](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L14)
60+
Defined in: [index.svelte.ts:19](https://github.com/TanStack/store/blob/main/packages/svelte-store/src/index.svelte.ts#L19)
5161

5262
### Type Parameters
5363

@@ -65,6 +75,10 @@ Defined in: [index.svelte.ts:14](https://github.com/TanStack/store/blob/main/pac
6575

6676
(`state`) => `TSelected`
6777

78+
#### options?
79+
80+
`UseStoreOptions`\<`TSelected`\>
81+
6882
### Returns
6983

7084
`object`

docs/framework/vue/reference/functions/shallow.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ title: shallow
1111
function shallow<T>(objA, objB): boolean
1212
```
1313

14-
Defined in: [index.ts:47](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L47)
14+
Defined in: [index.ts:55](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L55)
1515

1616
## Type Parameters
1717

docs/framework/vue/reference/functions/usestore.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ title: useStore
1010
## Call Signature
1111

1212
```ts
13-
function useStore<TState, TSelected>(store, selector?): Readonly<Ref<TSelected, TSelected>>
13+
function useStore<TState, TSelected>(
14+
store,
15+
selector?,
16+
options?): Readonly<Ref<TSelected, TSelected>>
1417
```
1518

16-
Defined in: [index.ts:12](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L12)
19+
Defined in: [index.ts:16](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L16)
1720

1821
### Type Parameters
1922

@@ -31,17 +34,24 @@ Defined in: [index.ts:12](https://github.com/TanStack/store/blob/main/packages/v
3134

3235
(`state`) => `TSelected`
3336

37+
#### options?
38+
39+
`UseStoreOptions`\<`TSelected`\>
40+
3441
### Returns
3542

3643
`Readonly`\<`Ref`\<`TSelected`, `TSelected`\>\>
3744

3845
## Call Signature
3946

4047
```ts
41-
function useStore<TState, TSelected>(store, selector?): Readonly<Ref<TSelected, TSelected>>
48+
function useStore<TState, TSelected>(
49+
store,
50+
selector?,
51+
options?): Readonly<Ref<TSelected, TSelected>>
4252
```
4353

44-
Defined in: [index.ts:16](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L16)
54+
Defined in: [index.ts:21](https://github.com/TanStack/store/blob/main/packages/vue-store/src/index.ts#L21)
4555

4656
### Type Parameters
4757

@@ -59,6 +69,10 @@ Defined in: [index.ts:16](https://github.com/TanStack/store/blob/main/packages/v
5969

6070
(`state`) => `TSelected`
6171

72+
#### options?
73+
74+
`UseStoreOptions`\<`TSelected`\>
75+
6276
### Returns
6377

6478
`Readonly`\<`Ref`\<`TSelected`, `TSelected`\>\>

packages/react-store/src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,33 @@ export * from '@tanstack/store'
77
* @private
88
*/
99
export type NoInfer<T> = [T][T extends any ? 0 : never]
10+
type EqualityFn<T> = (objA: T, objB: T) => boolean
11+
interface UseStoreOptions<T> {
12+
equal?: EqualityFn<T>
13+
}
1014

1115
export function useStore<TState, TSelected = NoInfer<TState>>(
1216
store: Store<TState, any>,
1317
selector?: (state: NoInfer<TState>) => TSelected,
18+
options?: UseStoreOptions<TSelected>,
1419
): TSelected
1520
export function useStore<TState, TSelected = NoInfer<TState>>(
1621
store: Derived<TState, any>,
1722
selector?: (state: NoInfer<TState>) => TSelected,
23+
options?: UseStoreOptions<TSelected>,
1824
): TSelected
1925
export function useStore<TState, TSelected = NoInfer<TState>>(
2026
store: Store<TState, any> | Derived<TState, any>,
2127
selector: (state: NoInfer<TState>) => TSelected = (d) => d as any,
28+
options: UseStoreOptions<TSelected> = {},
2229
): TSelected {
30+
const equal = options.equal ?? shallow
2331
const slice = useSyncExternalStoreWithSelector(
2432
store.subscribe,
2533
() => store.state,
2634
() => store.state,
2735
selector,
28-
shallow,
36+
equal,
2937
)
3038

3139
return slice

packages/react-store/tests/index.test.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,78 @@ describe('useStore', () => {
7878
expect(getByText('Number rendered: 2')).toBeInTheDocument()
7979
})
8080

81+
it('allow specifying custom equality function', async () => {
82+
const store = new Store({
83+
array: [
84+
{ select: 0, ignore: 1 },
85+
{ select: 0, ignore: 1 },
86+
],
87+
})
88+
89+
function deepEqual<T>(objA: T, objB: T) {
90+
return JSON.stringify(objA) === JSON.stringify(objB)
91+
}
92+
93+
function Comp() {
94+
const storeVal = useStore(
95+
store,
96+
(state) => state.array.map(({ ignore, ...rest }) => rest),
97+
{ equal: deepEqual },
98+
)
99+
const [fn] = useState(vi.fn)
100+
fn()
101+
102+
const value = storeVal
103+
.map((item) => item.select)
104+
.reduce((total, num) => total + num, 0)
105+
106+
return (
107+
<div>
108+
<p>Number rendered: {fn.mock.calls.length}</p>
109+
<p>Store: {value}</p>
110+
<button
111+
type="button"
112+
onClick={() =>
113+
store.setState((v) => ({
114+
array: v.array.map((item) => ({
115+
...item,
116+
select: item.select + 5,
117+
})),
118+
}))
119+
}
120+
>
121+
Update select
122+
</button>
123+
<button
124+
type="button"
125+
onClick={() =>
126+
store.setState((v) => ({
127+
array: v.array.map((item) => ({
128+
...item,
129+
ignore: item.ignore + 2,
130+
})),
131+
}))
132+
}
133+
>
134+
Update ignored
135+
</button>
136+
</div>
137+
)
138+
}
139+
140+
const { getByText } = render(<Comp />)
141+
expect(getByText('Store: 0')).toBeInTheDocument()
142+
expect(getByText('Number rendered: 1')).toBeInTheDocument()
143+
144+
await user.click(getByText('Update select'))
145+
146+
await waitFor(() => expect(getByText('Store: 10')).toBeInTheDocument())
147+
expect(getByText('Number rendered: 2')).toBeInTheDocument()
148+
149+
await user.click(getByText('Update ignored'))
150+
expect(getByText('Number rendered: 2')).toBeInTheDocument()
151+
})
152+
81153
it('works with mounted derived stores', async () => {
82154
const store = new Store(0)
83155

0 commit comments

Comments
 (0)