From 0cb47c8dde015cd4e42fe48f2b4b67472174903b Mon Sep 17 00:00:00 2001 From: Charles Kornoelje <33156025+charkour@users.noreply.github.com> Date: Fri, 8 Dec 2023 23:43:17 -0500 Subject: [PATCH 1/9] fix: remove deprecated v4 features --- src/context.ts | 102 -------------- src/index.ts | 1 - src/middleware/persist.ts | 219 +------------------------------ src/react.ts | 61 +-------- src/shallow.ts | 21 +-- src/vanilla.ts | 90 +------------ tests/basic.test.tsx | 15 --- tests/context.test.tsx | 174 ------------------------ tests/shallow.test.tsx | 5 +- tests/types.test.tsx | 2 - tests/vanilla/basic.test.ts | 16 --- tests/vanilla/subscribe.test.tsx | 123 +++++++++++++++++ 12 files changed, 132 insertions(+), 697 deletions(-) delete mode 100644 src/context.ts delete mode 100644 tests/context.test.tsx create mode 100644 tests/vanilla/subscribe.test.tsx diff --git a/src/context.ts b/src/context.ts deleted file mode 100644 index 01db464c33..0000000000 --- a/src/context.ts +++ /dev/null @@ -1,102 +0,0 @@ -// import { -// createElement, -// createContext as reactCreateContext, -// useContext, -// useMemo, -// useRef, -// } from 'react' -// That doesnt work in ESM, because React libs are CJS only. -// The following is a workaround until ESM is supported. -// eslint-disable-next-line import/extensions -import ReactExports from 'react' -import type { ReactNode } from 'react' -import type { StoreApi } from 'zustand' -// eslint-disable-next-line import/extensions -import { useStoreWithEqualityFn } from 'zustand/traditional' - -const { - createElement, - createContext: reactCreateContext, - useContext, - useMemo, - useRef, -} = ReactExports - -type UseContextStore> = { - (): ExtractState - ( - selector: (state: ExtractState) => U, - equalityFn?: (a: U, b: U) => boolean, - ): U -} - -type ExtractState = S extends { getState: () => infer T } ? T : never - -type WithoutCallSignature = { [K in keyof T]: T[K] } - -/** - * @deprecated Use `createStore` and `useStore` for context usage - */ -function createContext>() { - if (import.meta.env?.MODE !== 'production') { - console.warn( - "[DEPRECATED] `context` will be removed in a future version. Instead use `import { createStore, useStore } from 'zustand'`. See: https://github.com/pmndrs/zustand/discussions/1180.", - ) - } - const ZustandContext = reactCreateContext(undefined) - - const Provider = ({ - createStore, - children, - }: { - createStore: () => S - children: ReactNode - }) => { - const storeRef = useRef() - - if (!storeRef.current) { - storeRef.current = createStore() - } - - return createElement( - ZustandContext.Provider, - { value: storeRef.current }, - children, - ) - } - - const useContextStore: UseContextStore = >( - selector?: (state: ExtractState) => StateSlice, - equalityFn?: (a: StateSlice, b: StateSlice) => boolean, - ) => { - const store = useContext(ZustandContext) - if (!store) { - throw new Error( - 'Seems like you have not used zustand provider as an ancestor.', - ) - } - return useStoreWithEqualityFn( - store, - selector as (state: ExtractState) => StateSlice, - equalityFn, - ) - } - - const useStoreApi = () => { - const store = useContext(ZustandContext) - if (!store) { - throw new Error( - 'Seems like you have not used zustand provider as an ancestor.', - ) - } - return useMemo>(() => ({ ...store }), [store]) - } - - return { - Provider, - useStore: useContextStore, - useStoreApi, - } -} - -export default createContext diff --git a/src/index.ts b/src/index.ts index bc6544c82f..27b7a4f91f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,2 @@ export * from './vanilla.ts' export * from './react.ts' -export { default } from './react.ts' diff --git a/src/middleware/persist.ts b/src/middleware/persist.ts index 5d8ad27ff4..85f3d2d06f 100644 --- a/src/middleware/persist.ts +++ b/src/middleware/persist.ts @@ -66,34 +66,6 @@ export function createJSONStorage( export interface PersistOptions { /** Name of the storage (must be unique) */ name: string - /** - * @deprecated Use `storage` instead. - * A function returning a storage. - * The storage must fit `window.localStorage`'s api (or an async version of it). - * For example the storage could be `AsyncStorage` from React Native. - * - * @default () => localStorage - */ - getStorage?: () => StateStorage - /** - * @deprecated Use `storage` instead. - * Use a custom serializer. - * The returned string will be stored in the storage. - * - * @default JSON.stringify - */ - serialize?: (state: StorageValue) => string | Promise - /** - * @deprecated Use `storage` instead. - * Use a custom deserializer. - * Must return an object matching StorageValue - * - * @param str The storage's current value. - * @default JSON.parse - */ - deserialize?: ( - str: string, - ) => StorageValue | Promise> /** * Use a custom persist storage. * @@ -197,180 +169,7 @@ const toThenable = } } -const oldImpl: PersistImpl = (config, baseOptions) => (set, get, api) => { - type S = ReturnType - let options = { - getStorage: () => localStorage, - serialize: JSON.stringify as (state: StorageValue) => string, - deserialize: JSON.parse as (str: string) => StorageValue, - partialize: (state: S) => state, - version: 0, - merge: (persistedState: unknown, currentState: S) => ({ - ...currentState, - ...(persistedState as object), - }), - ...baseOptions, - } - - let hasHydrated = false - const hydrationListeners = new Set>() - const finishHydrationListeners = new Set>() - let storage: StateStorage | undefined - - try { - storage = options.getStorage() - } catch (e) { - // prevent error if the storage is not defined (e.g. when server side rendering a page) - } - - if (!storage) { - return config( - (...args) => { - console.warn( - `[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`, - ) - set(...args) - }, - get, - api, - ) - } - - const thenableSerialize = toThenable(options.serialize) - - const setItem = (): Thenable => { - const state = options.partialize({ ...get() }) - - let errorInSync: Error | undefined - const thenable = thenableSerialize({ state, version: options.version }) - .then((serializedValue) => - (storage as StateStorage).setItem(options.name, serializedValue), - ) - .catch((e) => { - errorInSync = e - }) - if (errorInSync) { - throw errorInSync - } - return thenable - } - - const savedSetState = api.setState - - api.setState = (state, replace) => { - savedSetState(state, replace) - void setItem() - } - - const configResult = config( - (...args) => { - set(...args) - void setItem() - }, - get, - api, - ) - - // a workaround to solve the issue of not storing rehydrated state in sync storage - // the set(state) value would be later overridden with initial state by create() - // to avoid this, we merge the state from localStorage into the initial state. - let stateFromStorage: S | undefined - - // rehydrate initial state with existing stored state - const hydrate = () => { - if (!storage) return - - hasHydrated = false - hydrationListeners.forEach((cb) => cb(get())) - - const postRehydrationCallback = - options.onRehydrateStorage?.(get()) || undefined - - // bind is used to avoid `TypeError: Illegal invocation` error - return toThenable(storage.getItem.bind(storage))(options.name) - .then((storageValue) => { - if (storageValue) { - return options.deserialize(storageValue) - } - }) - .then((deserializedStorageValue) => { - if (deserializedStorageValue) { - if ( - typeof deserializedStorageValue.version === 'number' && - deserializedStorageValue.version !== options.version - ) { - if (options.migrate) { - return options.migrate( - deserializedStorageValue.state, - deserializedStorageValue.version, - ) - } - console.error( - `State loaded from storage couldn't be migrated since no migrate function was provided`, - ) - } else { - return deserializedStorageValue.state - } - } - }) - .then((migratedState) => { - stateFromStorage = options.merge( - migratedState as S, - get() ?? configResult, - ) - - set(stateFromStorage as S, true) - return setItem() - }) - .then(() => { - postRehydrationCallback?.(stateFromStorage, undefined) - hasHydrated = true - finishHydrationListeners.forEach((cb) => cb(stateFromStorage as S)) - }) - .catch((e: Error) => { - postRehydrationCallback?.(undefined, e) - }) - } - - ;(api as StoreApi & StorePersist).persist = { - setOptions: (newOptions) => { - options = { - ...options, - ...newOptions, - } - - if (newOptions.getStorage) { - storage = newOptions.getStorage() - } - }, - clearStorage: () => { - storage?.removeItem(options.name) - }, - getOptions: () => options, - rehydrate: () => hydrate() as Promise, - hasHydrated: () => hasHydrated, - onHydrate: (cb) => { - hydrationListeners.add(cb) - - return () => { - hydrationListeners.delete(cb) - } - }, - onFinishHydration: (cb) => { - finishHydrationListeners.add(cb) - - return () => { - finishHydrationListeners.delete(cb) - } - }, - } - - hydrate() - - return stateFromStorage || configResult -} - -const newImpl: PersistImpl = (config, baseOptions) => (set, get, api) => { +const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => { type S = ReturnType let options = { storage: createJSONStorage(() => localStorage), @@ -538,22 +337,6 @@ const newImpl: PersistImpl = (config, baseOptions) => (set, get, api) => { return stateFromStorage || configResult } -const persistImpl: PersistImpl = (config, baseOptions) => { - if ( - 'getStorage' in baseOptions || - 'serialize' in baseOptions || - 'deserialize' in baseOptions - ) { - if (import.meta.env?.MODE !== 'production') { - console.warn( - '[DEPRECATED] `getStorage`, `serialize` and `deserialize` options are deprecated. Use `storage` option instead.', - ) - } - return oldImpl(config, baseOptions) - } - return newImpl(config, baseOptions) -} - type Persist = < T, Mps extends [StoreMutatorIdentifier, unknown][] = [], diff --git a/src/react.ts b/src/react.ts index 48a35e3f89..912a9398aa 100644 --- a/src/react.ts +++ b/src/react.ts @@ -26,8 +26,6 @@ type WithReact> = S & { getServerState?: () => ExtractState } -let didWarnAboutEqualityFn = false - export function useStore>>( api: S, ): ExtractState @@ -37,37 +35,15 @@ export function useStore>, U>( selector: (state: ExtractState) => U, ): U -/** - * @deprecated Use `useStoreWithEqualityFn` from 'zustand/traditional' - * https://github.com/pmndrs/zustand/discussions/1937 - */ -export function useStore>, U>( - api: S, - selector: (state: ExtractState) => U, - equalityFn: ((a: U, b: U) => boolean) | undefined, -): U - export function useStore( api: WithReact>, selector: (state: TState) => StateSlice = api.getState as any, - equalityFn?: (a: StateSlice, b: StateSlice) => boolean, ) { - if ( - import.meta.env?.MODE !== 'production' && - equalityFn && - !didWarnAboutEqualityFn - ) { - console.warn( - "[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937", - ) - didWarnAboutEqualityFn = true - } const slice = useSyncExternalStoreWithSelector( api.subscribe, api.getState, api.getServerState || api.getState, selector, - equalityFn, ) useDebugValue(slice) return slice @@ -76,13 +52,6 @@ export function useStore( export type UseBoundStore>> = { (): ExtractState (selector: (state: ExtractState) => U): U - /** - * @deprecated Use `createWithEqualityFn` from 'zustand/traditional' - */ - ( - selector: (state: ExtractState) => U, - equalityFn: (a: U, b: U) => boolean, - ): U } & S type Create = { @@ -92,26 +61,12 @@ type Create = { (): ( initializer: StateCreator, ) => UseBoundStore, Mos>> - /** - * @deprecated Use `useStore` hook to bind store - */ - >(store: S): UseBoundStore } const createImpl = (createState: StateCreator) => { - if ( - import.meta.env?.MODE !== 'production' && - typeof createState !== 'function' - ) { - console.warn( - "[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.", - ) - } - const api = - typeof createState === 'function' ? createStore(createState) : createState + const api = createStore(createState) - const useBoundStore: any = (selector?: any, equalityFn?: any) => - useStore(api, selector, equalityFn) + const useBoundStore: any = (selector?: any) => useStore(api, selector) Object.assign(useBoundStore, api) @@ -120,15 +75,3 @@ const createImpl = (createState: StateCreator) => { export const create = ((createState: StateCreator | undefined) => createState ? createImpl(createState) : createImpl) as Create - -/** - * @deprecated Use `import { create } from 'zustand'` - */ -export default ((createState: any) => { - if (import.meta.env?.MODE !== 'production') { - console.warn( - "[DEPRECATED] Default export is deprecated. Instead use `import { create } from 'zustand'`.", - ) - } - return create(createState) -}) as Create diff --git a/src/shallow.ts b/src/shallow.ts index 09e3ba5b3b..2fb316beae 100644 --- a/src/shallow.ts +++ b/src/shallow.ts @@ -1,19 +1,2 @@ -import { shallow } from './vanilla/shallow.ts' - -// We will export this in v5 and remove default export -// export { shallow } from './vanilla/shallow.ts' -// export { useShallow } from './react/shallow.ts' - -/** - * @deprecated Use `import { shallow } from 'zustand/shallow'` - */ -export default ((objA, objB) => { - if (import.meta.env?.MODE !== 'production') { - console.warn( - "[DEPRECATED] Default export is deprecated. Instead use `import { shallow } from 'zustand/shallow'`.", - ) - } - return shallow(objA, objB) -}) as typeof shallow - -export { shallow } +export { shallow } from './vanilla/shallow.ts' +export { useShallow } from './react/shallow.ts' diff --git a/src/vanilla.ts b/src/vanilla.ts index d5d9c092f0..f1a54b7e10 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -9,10 +9,6 @@ export interface StoreApi { setState: SetStateInternal getState: () => T subscribe: (listener: (state: T, prevState: T) => void) => () => void - /** - * @deprecated Use `unsubscribe` returned by `subscribe` - */ - destroy: () => void } type Get = K extends keyof T ? T[K] : F @@ -88,94 +84,10 @@ const createStoreImpl: CreateStoreImpl = (createState) => { return () => listeners.delete(listener) } - const destroy: StoreApi['destroy'] = () => { - if (import.meta.env?.MODE !== 'production') { - console.warn( - '[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected.', - ) - } - listeners.clear() - } - - const api = { setState, getState, subscribe, destroy } + const api = { setState, getState, subscribe } state = createState(setState, getState, api) return api as any } export const createStore = ((createState) => createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore - -/** - * @deprecated Use `import { createStore } from 'zustand/vanilla'` - */ -export default ((createState) => { - if (import.meta.env?.MODE !== 'production') { - console.warn( - "[DEPRECATED] Default export is deprecated. Instead use import { createStore } from 'zustand/vanilla'.", - ) - } - return createStore(createState) -}) as CreateStore - -// --------------------------------------------------------- - -/** - * @deprecated Use `unknown` instead of `State` - */ -export type State = unknown - -/** - * @deprecated Use `Partial | ((s: T) => Partial)` instead of `PartialState` - */ -export type PartialState = - | Partial - | ((state: T) => Partial) - -/** - * @deprecated Use `(s: T) => U` instead of `StateSelector` - */ -export type StateSelector = (state: T) => U - -/** - * @deprecated Use `(a: T, b: T) => boolean` instead of `EqualityChecker` - */ -export type EqualityChecker = (state: T, newState: T) => boolean - -/** - * @deprecated Use `(state: T, previousState: T) => void` instead of `StateListener` - */ -export type StateListener = (state: T, previousState: T) => void - -/** - * @deprecated Use `(slice: T, previousSlice: T) => void` instead of `StateSliceListener`. - */ -export type StateSliceListener = (slice: T, previousSlice: T) => void - -/** - * @deprecated Use `(listener: (state: T) => void) => void` instead of `Subscribe`. - */ -export type Subscribe = { - (listener: (state: T, previousState: T) => void): () => void -} - -/** - * @deprecated You might be looking for `StateCreator`, if not then - * use `StoreApi['setState']` instead of `SetState`. - */ -export type SetState = { - _( - partial: T | Partial | { _(state: T): T | Partial }['_'], - replace?: boolean | undefined, - ): void -}['_'] - -/** - * @deprecated You might be looking for `StateCreator`, if not then - * use `StoreApi['getState']` instead of `GetState`. - */ -export type GetState = () => T - -/** - * @deprecated Use `StoreApi['destroy']` instead of `Destroy`. - */ -export type Destroy = () => void diff --git a/tests/basic.test.tsx b/tests/basic.test.tsx index a73224c0ee..b1cf0133e1 100644 --- a/tests/basic.test.tsx +++ b/tests/basic.test.tsx @@ -30,7 +30,6 @@ it('creates a store hook and api object', () => { [Function], [Function], { - "destroy": [Function], "getState": [Function], "setState": [Function], "subscribe": [Function], @@ -472,20 +471,6 @@ it('can set the store without merging', () => { expect(getState()).toEqual({ b: 2 }) }) -it('can destroy the store', () => { - const { destroy, getState, setState, subscribe } = create(() => ({ - value: 1, - })) - - subscribe(() => { - throw new Error('did not clear listener on destroy') - }) - destroy() - - setState({ value: 2 }) - expect(getState().value).toEqual(2) -}) - it('only calls selectors when necessary', async () => { type State = { a: number; b: number } const useBoundStore = create(() => ({ a: 0, b: 0 })) diff --git a/tests/context.test.tsx b/tests/context.test.tsx deleted file mode 100644 index 5944f11773..0000000000 --- a/tests/context.test.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { - Component as ClassComponent, - ReactNode, - StrictMode, - useCallback, - useEffect, - useState, -} from 'react' -import { render } from '@testing-library/react' -import { afterEach, it, vi } from 'vitest' -import { create } from 'zustand' -import type { StoreApi } from 'zustand' -import createContext from 'zustand/context' -import { subscribeWithSelector } from 'zustand/middleware' - -const consoleError = console.error -afterEach(() => { - console.error = consoleError -}) - -type CounterState = { - count: number - inc: () => void -} - -it('creates and uses context store', async () => { - const { Provider, useStore } = createContext>() - - const createStore = () => - create((set) => ({ - count: 0, - inc: () => set((state) => ({ count: state.count + 1 })), - })) - - function Counter() { - const { count, inc } = useStore() - useEffect(inc, [inc]) - return
count: {count * 1}
- } - - const { findByText } = render( - <> - - - - , - ) - - await findByText('count: 1') -}) - -it('uses context store with selectors', async () => { - const { Provider, useStore } = createContext>() - - const createStore = () => - create((set) => ({ - count: 0, - inc: () => set((state) => ({ count: state.count + 1 })), - })) - - function Counter() { - const count = useStore((state) => state.count) - const inc = useStore((state) => state.inc) - useEffect(inc, [inc]) - return
count: {count * 1}
- } - - const { findByText } = render( - <> - - - - , - ) - - await findByText('count: 1') -}) - -it('uses context store api', async () => { - const createStore = () => - create()( - subscribeWithSelector((set) => ({ - count: 0, - inc: () => set((state) => ({ count: state.count + 1 })), - })), - ) - - type CustomStore = ReturnType - const { Provider, useStoreApi } = createContext() - - function Counter() { - const storeApi = useStoreApi() - const [count, setCount] = useState(0) - useEffect( - () => - storeApi.subscribe( - (state) => state.count, - () => setCount(storeApi.getState().count), - ), - [storeApi], - ) - useEffect(() => { - storeApi.setState({ count: storeApi.getState().count + 1 }) - }, [storeApi]) - useEffect(() => { - if (count === 1) { - storeApi.destroy() - storeApi.setState({ count: storeApi.getState().count + 1 }) - } - }, [storeApi, count]) - return
count: {count * 1}
- } - - const { findByText } = render( - <> - - - - , - ) - - await findByText('count: 1') -}) - -it('throws error when not using provider', async () => { - console.error = vi.fn() - - class ErrorBoundary extends ClassComponent< - { children?: ReactNode | undefined }, - { hasError: boolean } - > { - constructor(props: { children?: ReactNode | undefined }) { - super(props) - this.state = { hasError: false } - } - static getDerivedStateFromError() { - return { hasError: true } - } - render() { - return this.state.hasError ?
errored
: this.props.children - } - } - - const { useStore } = createContext>() - function Component() { - useStore() - return
no error
- } - - const { findByText } = render( - - - - - , - ) - await findByText('errored') -}) - -it('useCallback with useStore infers types correctly', async () => { - const { useStore } = createContext>() - function _Counter() { - const _x = useStore(useCallback((state) => state.count, [])) - expectAreTypesEqual().toBe(true) - } -}) - -const expectAreTypesEqual = () => ({ - toBe: ( - _: (() => T extends B ? 1 : 0) extends () => T extends A ? 1 : 0 - ? true - : false, - ) => {}, -}) diff --git a/tests/shallow.test.tsx b/tests/shallow.test.tsx index 9226a34a2d..65441f8efa 100644 --- a/tests/shallow.test.tsx +++ b/tests/shallow.test.tsx @@ -3,11 +3,12 @@ import { act, fireEvent, render } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' +import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' describe('types', () => { it('works with useBoundStore and array selector (#1107)', () => { - const useBoundStore = create(() => ({ + const useBoundStore = createWithEqualityFn(() => ({ villages: [] as { name: string }[], })) const Component = () => { @@ -18,7 +19,7 @@ describe('types', () => { }) it('works with useBoundStore and string selector (#1107)', () => { - const useBoundStore = create(() => ({ + const useBoundStore = createWithEqualityFn(() => ({ refetchTimestamp: '', })) const Component = () => { diff --git a/tests/types.test.tsx b/tests/types.test.tsx index 0b8db94877..835ba48f8d 100644 --- a/tests/types.test.tsx +++ b/tests/types.test.tsx @@ -79,7 +79,6 @@ it('can use exposed types', () => { _stateSelector: (state: ExampleState) => number, _storeApi: StoreApi, _subscribe: StoreApi['subscribe'], - _destroy: StoreApi['destroy'], _equalityFn: (a: ExampleState, b: ExampleState) => boolean, _stateCreator: StateCreator, _useBoundStore: UseBoundStore>, @@ -96,7 +95,6 @@ it('can use exposed types', () => { selector, storeApi, storeApi.subscribe, - storeApi.destroy, equalityFn, stateCreator, useBoundStore, diff --git a/tests/vanilla/basic.test.ts b/tests/vanilla/basic.test.ts index 9258db17d0..7ddc0e42ec 100644 --- a/tests/vanilla/basic.test.ts +++ b/tests/vanilla/basic.test.ts @@ -22,14 +22,12 @@ it('create a store', () => { [Function], [Function], { - "destroy": [Function], "getState": [Function], "setState": [Function], "subscribe": [Function], }, ], "result": { - "destroy": [Function], "getState": [Function], "setState": [Function], "subscribe": [Function], @@ -138,17 +136,3 @@ it('works with non-object state', () => { expect(store.getState()).toBe(2) }) - -it('can destroy the store', () => { - const { destroy, getState, setState, subscribe } = createStore(() => ({ - value: 1, - })) - - subscribe(() => { - throw new Error('did not clear listener on destroy') - }) - destroy() - - setState({ value: 2 }) - expect(getState().value).toEqual(2) -}) diff --git a/tests/vanilla/subscribe.test.tsx b/tests/vanilla/subscribe.test.tsx new file mode 100644 index 0000000000..0210672b09 --- /dev/null +++ b/tests/vanilla/subscribe.test.tsx @@ -0,0 +1,123 @@ +import { describe, expect, it, vi } from 'vitest' +import { subscribeWithSelector } from 'zustand/middleware' +import { createStore } from 'zustand/vanilla' + +describe('subscribe()', () => { + it('should not be called if new state identity is the same', () => { + const spy = vi.fn() + const initialState = { value: 1, other: 'a' } + const { setState, subscribe } = createStore(() => initialState) + + subscribe(spy) + setState(initialState) + expect(spy).not.toHaveBeenCalled() + }) + + it('should be called if new state identity is different', () => { + const spy = vi.fn() + const initialState = { value: 1, other: 'a' } + const { setState, getState, subscribe } = createStore(() => initialState) + + subscribe(spy) + setState({ ...getState() }) + expect(spy).toHaveBeenCalledWith(initialState, initialState) + }) + + it('should not be called when state slice is the same', () => { + const spy = vi.fn() + const initialState = { value: 1, other: 'a' } + const { setState, subscribe } = createStore( + subscribeWithSelector(() => initialState), + ) + + subscribe((s) => s.value, spy) + setState({ other: 'b' }) + expect(spy).not.toHaveBeenCalled() + }) + + it('should be called when state slice changes', () => { + const spy = vi.fn() + const initialState = { value: 1, other: 'a' } + const { setState, subscribe } = createStore( + subscribeWithSelector(() => initialState), + ) + + subscribe((s) => s.value, spy) + setState({ value: initialState.value + 1 }) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith(initialState.value + 1, initialState.value) + }) + + it('should not be called when equality checker returns true', () => { + const spy = vi.fn() + const initialState = { value: 1, other: 'a' } + const { setState, subscribe } = createStore( + subscribeWithSelector(() => initialState), + ) + + subscribe((s) => s, spy, { equalityFn: () => true }) + setState({ value: initialState.value + 2 }) + expect(spy).not.toHaveBeenCalled() + }) + + it('should be called when equality checker returns false', () => { + const spy = vi.fn() + const initialState = { value: 1, other: 'a' } + const { setState, subscribe } = createStore( + subscribeWithSelector(() => initialState), + ) + + subscribe((s) => s.value, spy, { equalityFn: () => false }) + setState({ value: initialState.value + 2 }) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith(initialState.value + 2, initialState.value) + }) + + it('should unsubscribe correctly', () => { + const spy = vi.fn() + const initialState = { value: 1, other: 'a' } + const { setState, subscribe } = createStore( + subscribeWithSelector(() => initialState), + ) + + const unsub = subscribe((s) => s.value, spy) + + setState({ value: initialState.value + 1 }) + unsub() + setState({ value: initialState.value + 2 }) + + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith(initialState.value + 1, initialState.value) + }) + + it('should keep consistent behavior with equality check', () => { + const spy = vi.fn() + const initialState = { value: 1, other: 'a' } + const { getState, setState, subscribe } = createStore( + subscribeWithSelector(() => initialState), + ) + + const isRoughEqual = (x: number, y: number) => Math.abs(x - y) < 1 + setState({ value: 0 }) + spy.mockReset() + const spy2 = vi.fn() + let prevValue = getState().value + const unsub = subscribe((s) => { + if (isRoughEqual(prevValue, s.value)) { + // skip assuming values are equal + return + } + spy(s.value, prevValue) + prevValue = s.value + }) + const unsub2 = subscribe((s) => s.value, spy2, { equalityFn: isRoughEqual }) + setState({ value: 0.5 }) + setState({ value: 1 }) + unsub() + unsub2() + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith(1, 0) + expect(spy2).toHaveBeenCalledTimes(1) + expect(spy2).toHaveBeenCalledWith(1, 0) + }) +}) From 3b9c75d89cc6872fb1dddeb0bd3a0aa527725ef2 Mon Sep 17 00:00:00 2001 From: Charles Kornoelje <33156025+charkour@users.noreply.github.com> Date: Sat, 9 Dec 2023 00:05:51 -0500 Subject: [PATCH 2/9] chore(build): remove context --- package.json | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/package.json b/package.json index 34c23ba28b..d1798b1dc4 100644 --- a/package.json +++ b/package.json @@ -134,20 +134,6 @@ "types": "./traditional.d.ts", "default": "./traditional.js" } - }, - "./context": { - "import": { - "types": "./esm/context.d.mts", - "default": "./esm/context.mjs" - }, - "module": { - "types": "./esm/context.d.ts", - "default": "./esm/context.js" - }, - "default": { - "types": "./context.d.ts", - "default": "./context.js" - } } }, "sideEffects": false, @@ -162,7 +148,6 @@ "build:vanilla:shallow": "rollup -c --config-vanilla_shallow", "build:react:shallow": "rollup -c --config-react_shallow", "build:traditional": "rollup -c --config-traditional", - "build:context": "rollup -c --config-context", "postbuild": "yarn patch-d-ts && yarn copy && yarn patch-esm-ts", "prettier": "prettier \"*.{js,json,md}\" \"{examples,src,tests,docs}/**/*.{js,jsx,ts,tsx,md,mdx}\" --write", "prettier:ci": "prettier '*.{js,json,md}' '{examples,src,tests,docs}/**/*.{js,jsx,ts,tsx,md,mdx}' --list-different", From 5e079b02e5a87fde1a88c2e5ae23b2efbd95d52b Mon Sep 17 00:00:00 2001 From: Charles Kornoelje <33156025+charkour@users.noreply.github.com> Date: Sat, 9 Dec 2023 00:06:22 -0500 Subject: [PATCH 3/9] docs(typescript): remove deprecated equals api --- docs/guides/typescript.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/guides/typescript.md b/docs/guides/typescript.md index f3188a9e88..b5b889ed6b 100644 --- a/docs/guides/typescript.md +++ b/docs/guides/typescript.md @@ -445,13 +445,11 @@ const bearStore = createStore()((set) => ({ function useBearStore(): BearState function useBearStore( selector: (state: BearState) => T, - equals?: (a: T, b: T) => boolean, ): T function useBearStore( selector?: (state: BearState) => T, - equals?: (a: T, b: T) => boolean, ) { - return useStore(bearStore, selector!, equals) + return useStore(bearStore, selector!) } ``` @@ -471,14 +469,13 @@ const bearStore = createStore()((set) => ({ increase: (by) => set((state) => ({ bears: state.bears + by })), })) -const createBoundedUseStore = ((store) => (selector, equals) => - useStore(store, selector as never, equals)) as >( +const createBoundedUseStore = ((store) => (selector) => + useStore(store, selector as never)) as >( store: S, ) => { (): ExtractState ( selector: (state: ExtractState) => T, - equals?: (a: T, b: T) => boolean, ): T } From 47bfd7ac47bb9d880ce90ade89a69cc5b94c80c8 Mon Sep 17 00:00:00 2001 From: Charles Kornoelje <33156025+charkour@users.noreply.github.com> Date: Sat, 9 Dec 2023 00:06:38 -0500 Subject: [PATCH 4/9] docs(persist): remove old persist api --- docs/integrations/persisting-store-data.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/docs/integrations/persisting-store-data.md b/docs/integrations/persisting-store-data.md index 5929e6ccb9..085d7ab2d8 100644 --- a/docs/integrations/persisting-store-data.md +++ b/docs/integrations/persisting-store-data.md @@ -630,7 +630,7 @@ export const useBoundStore = create( ) ``` -If you're using a type that JSON.stringify() doesn't support, you'll need to write your own serialization/deserialization code. However, if this is tedious, you can use third-party libraries to serialize and deserialize different types of data. +If you're using a type that `JSON.stringify()` doesn't support, you'll need to write your own serialization/deserialization code. However, if this is tedious, you can use third-party libraries to serialize and deserialize different types of data. For example, [Superjson](https://github.com/blitz-js/superjson) can serialize data along with its type, allowing the data to be parsed back to its original type upon deserialization @@ -735,15 +735,8 @@ export const useBearStore = create()( ### How do I use it with Map and Set -With the previous persist API, you would use `serialize`/`deserialize` -to deal with `Map` and `Set` and convert them into -an Array so they could be parsed into proper JSON. - -The new persist API has deprecated `serialize`/`deserialize`. - -Now, you will need to use the `storage` prop. -Let's say your state uses `Map` to handle a list of `transactions`, -then you can convert the Map into an Array in the storage prop: +If your state uses `Map` or `Set` to handle a list of `transactions`, +then you can convert the `Map` into an Array in the `storage` prop: ```ts From 7caba56305558a1951f10a0402f39f5511649d53 Mon Sep 17 00:00:00 2001 From: Charles Kornoelje <33156025+charkour@users.noreply.github.com> Date: Sat, 9 Dec 2023 00:11:33 -0500 Subject: [PATCH 5/9] chore: run yarn prettier on typescript docs --- docs/guides/typescript.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/guides/typescript.md b/docs/guides/typescript.md index b5b889ed6b..8934762eea 100644 --- a/docs/guides/typescript.md +++ b/docs/guides/typescript.md @@ -443,12 +443,8 @@ const bearStore = createStore()((set) => ({ })) function useBearStore(): BearState -function useBearStore( - selector: (state: BearState) => T, -): T -function useBearStore( - selector?: (state: BearState) => T, -) { +function useBearStore(selector: (state: BearState) => T): T +function useBearStore(selector?: (state: BearState) => T) { return useStore(bearStore, selector!) } ``` @@ -474,9 +470,7 @@ const createBoundedUseStore = ((store) => (selector) => store: S, ) => { (): ExtractState - ( - selector: (state: ExtractState) => T, - ): T + (selector: (state: ExtractState) => T): T } type ExtractState = S extends { getState: () => infer X } ? X : never From deace20addd65f6a2f39404eb4489f13a40c3975 Mon Sep 17 00:00:00 2001 From: Charles Kornoelje <33156025+charkour@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:58:13 -0600 Subject: [PATCH 6/9] Discard changes to docs/guides/typescript.md --- docs/guides/typescript.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/guides/typescript.md b/docs/guides/typescript.md index 8934762eea..f3188a9e88 100644 --- a/docs/guides/typescript.md +++ b/docs/guides/typescript.md @@ -443,9 +443,15 @@ const bearStore = createStore()((set) => ({ })) function useBearStore(): BearState -function useBearStore(selector: (state: BearState) => T): T -function useBearStore(selector?: (state: BearState) => T) { - return useStore(bearStore, selector!) +function useBearStore( + selector: (state: BearState) => T, + equals?: (a: T, b: T) => boolean, +): T +function useBearStore( + selector?: (state: BearState) => T, + equals?: (a: T, b: T) => boolean, +) { + return useStore(bearStore, selector!, equals) } ``` @@ -465,12 +471,15 @@ const bearStore = createStore()((set) => ({ increase: (by) => set((state) => ({ bears: state.bears + by })), })) -const createBoundedUseStore = ((store) => (selector) => - useStore(store, selector as never)) as >( +const createBoundedUseStore = ((store) => (selector, equals) => + useStore(store, selector as never, equals)) as >( store: S, ) => { (): ExtractState - (selector: (state: ExtractState) => T): T + ( + selector: (state: ExtractState) => T, + equals?: (a: T, b: T) => boolean, + ): T } type ExtractState = S extends { getState: () => infer X } ? X : never From 04f6e03c9122484e9a1c679d5bb7c517a164069a Mon Sep 17 00:00:00 2001 From: Charles Kornoelje <33156025+charkour@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:58:20 -0600 Subject: [PATCH 7/9] Discard changes to docs/integrations/persisting-store-data.md --- docs/integrations/persisting-store-data.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/integrations/persisting-store-data.md b/docs/integrations/persisting-store-data.md index 085d7ab2d8..5929e6ccb9 100644 --- a/docs/integrations/persisting-store-data.md +++ b/docs/integrations/persisting-store-data.md @@ -630,7 +630,7 @@ export const useBoundStore = create( ) ``` -If you're using a type that `JSON.stringify()` doesn't support, you'll need to write your own serialization/deserialization code. However, if this is tedious, you can use third-party libraries to serialize and deserialize different types of data. +If you're using a type that JSON.stringify() doesn't support, you'll need to write your own serialization/deserialization code. However, if this is tedious, you can use third-party libraries to serialize and deserialize different types of data. For example, [Superjson](https://github.com/blitz-js/superjson) can serialize data along with its type, allowing the data to be parsed back to its original type upon deserialization @@ -735,8 +735,15 @@ export const useBearStore = create()( ### How do I use it with Map and Set -If your state uses `Map` or `Set` to handle a list of `transactions`, -then you can convert the `Map` into an Array in the `storage` prop: +With the previous persist API, you would use `serialize`/`deserialize` +to deal with `Map` and `Set` and convert them into +an Array so they could be parsed into proper JSON. + +The new persist API has deprecated `serialize`/`deserialize`. + +Now, you will need to use the `storage` prop. +Let's say your state uses `Map` to handle a list of `transactions`, +then you can convert the Map into an Array in the storage prop: ```ts From ccb7f6e0738df804688adddca291745834691efe Mon Sep 17 00:00:00 2001 From: Charles Kornoelje <33156025+charkour@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:58:43 -0600 Subject: [PATCH 8/9] Discard changes to tests/shallow.test.tsx --- tests/shallow.test.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/shallow.test.tsx b/tests/shallow.test.tsx index 65441f8efa..9226a34a2d 100644 --- a/tests/shallow.test.tsx +++ b/tests/shallow.test.tsx @@ -3,12 +3,11 @@ import { act, fireEvent, render } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' -import { createWithEqualityFn } from 'zustand/traditional' import { shallow } from 'zustand/vanilla/shallow' describe('types', () => { it('works with useBoundStore and array selector (#1107)', () => { - const useBoundStore = createWithEqualityFn(() => ({ + const useBoundStore = create(() => ({ villages: [] as { name: string }[], })) const Component = () => { @@ -19,7 +18,7 @@ describe('types', () => { }) it('works with useBoundStore and string selector (#1107)', () => { - const useBoundStore = createWithEqualityFn(() => ({ + const useBoundStore = create(() => ({ refetchTimestamp: '', })) const Component = () => { From da4164d20e21bbc018c5de342a57b074c6c24351 Mon Sep 17 00:00:00 2001 From: Charles Kornoelje <33156025+charkour@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:58:54 -0600 Subject: [PATCH 9/9] Discard changes to tests/vanilla/subscribe.test.tsx --- tests/vanilla/subscribe.test.tsx | 123 ------------------------------- 1 file changed, 123 deletions(-) delete mode 100644 tests/vanilla/subscribe.test.tsx diff --git a/tests/vanilla/subscribe.test.tsx b/tests/vanilla/subscribe.test.tsx deleted file mode 100644 index 0210672b09..0000000000 --- a/tests/vanilla/subscribe.test.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { subscribeWithSelector } from 'zustand/middleware' -import { createStore } from 'zustand/vanilla' - -describe('subscribe()', () => { - it('should not be called if new state identity is the same', () => { - const spy = vi.fn() - const initialState = { value: 1, other: 'a' } - const { setState, subscribe } = createStore(() => initialState) - - subscribe(spy) - setState(initialState) - expect(spy).not.toHaveBeenCalled() - }) - - it('should be called if new state identity is different', () => { - const spy = vi.fn() - const initialState = { value: 1, other: 'a' } - const { setState, getState, subscribe } = createStore(() => initialState) - - subscribe(spy) - setState({ ...getState() }) - expect(spy).toHaveBeenCalledWith(initialState, initialState) - }) - - it('should not be called when state slice is the same', () => { - const spy = vi.fn() - const initialState = { value: 1, other: 'a' } - const { setState, subscribe } = createStore( - subscribeWithSelector(() => initialState), - ) - - subscribe((s) => s.value, spy) - setState({ other: 'b' }) - expect(spy).not.toHaveBeenCalled() - }) - - it('should be called when state slice changes', () => { - const spy = vi.fn() - const initialState = { value: 1, other: 'a' } - const { setState, subscribe } = createStore( - subscribeWithSelector(() => initialState), - ) - - subscribe((s) => s.value, spy) - setState({ value: initialState.value + 1 }) - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith(initialState.value + 1, initialState.value) - }) - - it('should not be called when equality checker returns true', () => { - const spy = vi.fn() - const initialState = { value: 1, other: 'a' } - const { setState, subscribe } = createStore( - subscribeWithSelector(() => initialState), - ) - - subscribe((s) => s, spy, { equalityFn: () => true }) - setState({ value: initialState.value + 2 }) - expect(spy).not.toHaveBeenCalled() - }) - - it('should be called when equality checker returns false', () => { - const spy = vi.fn() - const initialState = { value: 1, other: 'a' } - const { setState, subscribe } = createStore( - subscribeWithSelector(() => initialState), - ) - - subscribe((s) => s.value, spy, { equalityFn: () => false }) - setState({ value: initialState.value + 2 }) - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith(initialState.value + 2, initialState.value) - }) - - it('should unsubscribe correctly', () => { - const spy = vi.fn() - const initialState = { value: 1, other: 'a' } - const { setState, subscribe } = createStore( - subscribeWithSelector(() => initialState), - ) - - const unsub = subscribe((s) => s.value, spy) - - setState({ value: initialState.value + 1 }) - unsub() - setState({ value: initialState.value + 2 }) - - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith(initialState.value + 1, initialState.value) - }) - - it('should keep consistent behavior with equality check', () => { - const spy = vi.fn() - const initialState = { value: 1, other: 'a' } - const { getState, setState, subscribe } = createStore( - subscribeWithSelector(() => initialState), - ) - - const isRoughEqual = (x: number, y: number) => Math.abs(x - y) < 1 - setState({ value: 0 }) - spy.mockReset() - const spy2 = vi.fn() - let prevValue = getState().value - const unsub = subscribe((s) => { - if (isRoughEqual(prevValue, s.value)) { - // skip assuming values are equal - return - } - spy(s.value, prevValue) - prevValue = s.value - }) - const unsub2 = subscribe((s) => s.value, spy2, { equalityFn: isRoughEqual }) - setState({ value: 0.5 }) - setState({ value: 1 }) - unsub() - unsub2() - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith(1, 0) - expect(spy2).toHaveBeenCalledTimes(1) - expect(spy2).toHaveBeenCalledWith(1, 0) - }) -})