Skip to content

Commit c6900a4

Browse files
authored
feat: deprecate equalityFn and add createWithEqualityFn (#1945)
* feat: deprecate equalityFn and add createWithEqualityFn * add link to deprecation message
1 parent 6d9c0cf commit c6900a4

File tree

5 files changed

+145
-8
lines changed

5 files changed

+145
-8
lines changed

package.json

+10
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@
6565
"module": "./esm/shallow.js",
6666
"default": "./shallow.js"
6767
},
68+
"./traditional": {
69+
"types": "./traditional.d.ts",
70+
"import": {
71+
"types": "./esm/traditional.d.mts",
72+
"default": "./esm/traditional.mjs"
73+
},
74+
"module": "./esm/traditional.js",
75+
"default": "./traditional.js"
76+
},
6877
"./context": {
6978
"types": "./context.d.ts",
7079
"import": {
@@ -84,6 +93,7 @@
8493
"build:middleware": "rollup -c --config-middleware",
8594
"build:middleware:immer": "rollup -c --config-middleware_immer",
8695
"build:shallow": "rollup -c --config-shallow",
96+
"build:traditional": "rollup -c --config-traditional",
8797
"build:context": "rollup -c --config-context",
8898
"postbuild": "yarn patch-d-ts && yarn copy && yarn patch-esm-ts",
8999
"prettier": "prettier \"*.{js,json,md}\" \"{examples,src,tests,docs}/**/*.{js,jsx,ts,tsx,md,mdx}\" --write",

src/context.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import {
66
useRef,
77
} from 'react'
88
import type { ReactNode } from 'react'
9-
import { useStore } from 'zustand'
109
import type { StoreApi } from 'zustand'
10+
// eslint-disable-next-line import/extensions
11+
import { useStoreWithEqualityFn } from 'zustand/traditional'
1112

1213
type UseContextStore<S extends StoreApi<unknown>> = {
1314
(): ExtractState<S>
@@ -62,7 +63,7 @@ function createContext<S extends StoreApi<unknown>>() {
6263
'Seems like you have not used zustand provider as an ancestor.'
6364
)
6465
}
65-
return useStore(
66+
return useStoreWithEqualityFn(
6667
store,
6768
selector as (state: ExtractState<S>) => StateSlice,
6869
equalityFn

src/react.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,31 @@ export function useStore<S extends WithReact<StoreApi<unknown>>>(
2727
api: S
2828
): ExtractState<S>
2929

30+
export function useStore<S extends WithReact<StoreApi<unknown>>, U>(
31+
api: S,
32+
selector: (state: ExtractState<S>) => U
33+
): U
34+
35+
/**
36+
* @deprecated Use `useStoreWithEqualityFn` from 'zustand/traditional'
37+
* https://github.com/pmndrs/zustand/discussions/1937
38+
*/
3039
export function useStore<S extends WithReact<StoreApi<unknown>>, U>(
3140
api: S,
3241
selector: (state: ExtractState<S>) => U,
33-
equalityFn?: (a: U, b: U) => boolean
42+
equalityFn: (a: U, b: U) => boolean
3443
): U
3544

3645
export function useStore<TState, StateSlice>(
3746
api: WithReact<StoreApi<TState>>,
3847
selector: (state: TState) => StateSlice = api.getState as any,
3948
equalityFn?: (a: StateSlice, b: StateSlice) => boolean
4049
) {
50+
if (import.meta.env?.MODE !== 'production' && equalityFn) {
51+
console.warn(
52+
"[DEPRECATED] Use `createWithEqualityFn` from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937"
53+
)
54+
}
4155
const slice = useSyncExternalStoreWithSelector(
4256
api.subscribe,
4357
api.getState,
@@ -51,9 +65,13 @@ export function useStore<TState, StateSlice>(
5165

5266
export type UseBoundStore<S extends WithReact<ReadonlyStoreApi<unknown>>> = {
5367
(): ExtractState<S>
68+
<U>(selector: (state: ExtractState<S>) => U): U
69+
/**
70+
* @deprecated Use `createWithEqualityFn` from 'zustand/traditional'
71+
*/
5472
<U>(
5573
selector: (state: ExtractState<S>) => U,
56-
equals?: (a: U, b: U) => boolean
74+
equalityFn: (a: U, b: U) => boolean
5775
): U
5876
} & S
5977

src/traditional.ts

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { useDebugValue } from 'react'
2+
// import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
3+
// This doesn't work in ESM, because use-sync-external-store only exposes CJS.
4+
// See: https://github.com/pmndrs/valtio/issues/452
5+
// The following is a workaround until ESM is supported.
6+
// eslint-disable-next-line import/extensions
7+
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector'
8+
import { createStore } from './vanilla.ts'
9+
import type {
10+
Mutate,
11+
StateCreator,
12+
StoreApi,
13+
StoreMutatorIdentifier,
14+
} from './vanilla.ts'
15+
16+
const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports
17+
18+
type ExtractState<S> = S extends { getState: () => infer T } ? T : never
19+
20+
type ReadonlyStoreApi<T> = Pick<StoreApi<T>, 'getState' | 'subscribe'>
21+
22+
type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {
23+
getServerState?: () => ExtractState<S>
24+
}
25+
26+
export function useStoreWithEqualityFn<S extends WithReact<StoreApi<unknown>>>(
27+
api: S
28+
): ExtractState<S>
29+
30+
export function useStoreWithEqualityFn<
31+
S extends WithReact<StoreApi<unknown>>,
32+
U
33+
>(
34+
api: S,
35+
selector: (state: ExtractState<S>) => U,
36+
equalityFn?: (a: U, b: U) => boolean
37+
): U
38+
39+
export function useStoreWithEqualityFn<TState, StateSlice>(
40+
api: WithReact<StoreApi<TState>>,
41+
selector: (state: TState) => StateSlice = api.getState as any,
42+
equalityFn?: (a: StateSlice, b: StateSlice) => boolean
43+
) {
44+
const slice = useSyncExternalStoreWithSelector(
45+
api.subscribe,
46+
api.getState,
47+
api.getServerState || api.getState,
48+
selector,
49+
equalityFn
50+
)
51+
useDebugValue(slice)
52+
return slice
53+
}
54+
55+
export type UseBoundStoreWithEqualityFn<
56+
S extends WithReact<ReadonlyStoreApi<unknown>>
57+
> = {
58+
(): ExtractState<S>
59+
<U>(
60+
selector: (state: ExtractState<S>) => U,
61+
equalityFn?: (a: U, b: U) => boolean
62+
): U
63+
} & S
64+
65+
type CreateWithEqualityFn = {
66+
<T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
67+
initializer: StateCreator<T, [], Mos>,
68+
defaultEqualityFn: <U>(a: U, b: U) => boolean
69+
): UseBoundStoreWithEqualityFn<Mutate<StoreApi<T>, Mos>>
70+
<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
71+
initializer: StateCreator<T, [], Mos>,
72+
defaultEqualityFn: <U>(a: U, b: U) => boolean
73+
) => UseBoundStoreWithEqualityFn<Mutate<StoreApi<T>, Mos>>
74+
}
75+
76+
const createWithEqualityFnImpl = <T>(
77+
createState: StateCreator<T, [], []>,
78+
defaultEqualityFn?: <U>(a: U, b: U) => boolean
79+
) => {
80+
const api = createStore(createState)
81+
82+
const useBoundStoreWithEqualityFn: any = (
83+
selector?: any,
84+
equalityFn = defaultEqualityFn
85+
) => useStoreWithEqualityFn(api, selector, equalityFn)
86+
87+
Object.assign(useBoundStoreWithEqualityFn, api)
88+
89+
return useBoundStoreWithEqualityFn
90+
}
91+
92+
export const createWithEqualityFn = (<T>(
93+
createState: StateCreator<T, [], []> | undefined,
94+
defaultEqualityFn?: <U>(a: U, b: U) => boolean
95+
) =>
96+
createState
97+
? createWithEqualityFnImpl(createState, defaultEqualityFn)
98+
: createWithEqualityFnImpl) as CreateWithEqualityFn

tests/basic.test.tsx

+14-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ReactDOM from 'react-dom'
1111
import { afterEach, expect, it, vi } from 'vitest'
1212
import { create } from 'zustand'
1313
import type { StoreApi } from 'zustand'
14+
import { createWithEqualityFn } from 'zustand/traditional'
1415

1516
const consoleError = console.error
1617
afterEach(() => {
@@ -89,7 +90,10 @@ it('uses the store with selectors', async () => {
8990
})
9091

9192
it('uses the store with a selector and equality checker', async () => {
92-
const useBoundStore = create(() => ({ item: { value: 0 } }))
93+
const useBoundStore = createWithEqualityFn(
94+
() => ({ item: { value: 0 } }),
95+
Object.is
96+
)
9397
const { setState } = useBoundStore
9498
let renderCount = 0
9599

@@ -214,7 +218,10 @@ it('can update the selector', async () => {
214218
it('can update the equality checker', async () => {
215219
type State = { value: number }
216220
type Props = { equalityFn: (a: State, b: State) => boolean }
217-
const useBoundStore = create<State>(() => ({ value: 0 }))
221+
const useBoundStore = createWithEqualityFn<State>(
222+
() => ({ value: 0 }),
223+
Object.is
224+
)
218225
const { setState } = useBoundStore
219226
const selector = (s: State) => s
220227

@@ -258,7 +265,10 @@ it('can call useBoundStore with progressively more arguments', async () => {
258265
equalityFn?: (a: number, b: number) => boolean
259266
}
260267

261-
const useBoundStore = create<State>(() => ({ value: 0 }))
268+
const useBoundStore = createWithEqualityFn<State>(
269+
() => ({ value: 0 }),
270+
Object.is
271+
)
262272
const { setState } = useBoundStore
263273

264274
let renderCount = 0
@@ -357,7 +367,7 @@ it('can throw an error in equality checker', async () => {
357367
type State = { value: string | number }
358368

359369
const initialState: State = { value: 'foo' }
360-
const useBoundStore = create(() => initialState)
370+
const useBoundStore = createWithEqualityFn(() => initialState, Object.is)
361371
const { setState } = useBoundStore
362372
const selector = (s: State) => s
363373
const equalityFn = (a: State, b: State) =>

0 commit comments

Comments
 (0)