diff --git a/package.json b/package.json index 5f03925c12..fa31362daa 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", diff --git a/src/context.ts b/src/context.ts deleted file mode 100644 index c80c4da462..0000000000 --- a/src/context.ts +++ /dev/null @@ -1,100 +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 - */ -export 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, - } -} 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 e3dae514d3..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) diff --git a/src/vanilla.ts b/src/vanilla.ts index 6c82a682d0..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,82 +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 `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 6ccabd2606..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/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) -})