[v5] breaking: drop deprecated features (#2235)

* fix: remove deprecated v4 features

* chore(build): remove context

* docs(typescript): remove deprecated equals api

* docs(persist): remove old persist api

* chore: run yarn prettier on typescript docs

* Discard changes to docs/guides/typescript.md

* Discard changes to docs/integrations/persisting-store-data.md

* Discard changes to tests/shallow.test.tsx

* Discard changes to tests/vanilla/subscribe.test.tsx
This commit is contained in:
Charles Kornoelje 2023-12-14 09:00:48 -06:00 committed by GitHub
parent 5c99468909
commit 6f99da1274
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 4 additions and 664 deletions

View File

@ -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",

View File

@ -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<S extends StoreApi<unknown>> = {
(): ExtractState<S>
<U>(
selector: (state: ExtractState<S>) => U,
equalityFn?: (a: U, b: U) => boolean,
): U
}
type ExtractState<S> = S extends { getState: () => infer T } ? T : never
type WithoutCallSignature<T> = { [K in keyof T]: T[K] }
/**
* @deprecated Use `createStore` and `useStore` for context usage
*/
export function createContext<S extends StoreApi<unknown>>() {
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<S | undefined>(undefined)
const Provider = ({
createStore,
children,
}: {
createStore: () => S
children: ReactNode
}) => {
const storeRef = useRef<S>()
if (!storeRef.current) {
storeRef.current = createStore()
}
return createElement(
ZustandContext.Provider,
{ value: storeRef.current },
children,
)
}
const useContextStore: UseContextStore<S> = <StateSlice = ExtractState<S>>(
selector?: (state: ExtractState<S>) => 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<S>) => 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<WithoutCallSignature<S>>(() => ({ ...store }), [store])
}
return {
Provider,
useStore: useContextStore,
useStoreApi,
}
}

View File

@ -66,34 +66,6 @@ export function createJSONStorage<S>(
export interface PersistOptions<S, PersistedState = S> {
/** 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<S>) => string | Promise<string>
/**
* @deprecated Use `storage` instead.
* Use a custom deserializer.
* Must return an object matching StorageValue<S>
*
* @param str The storage's current value.
* @default JSON.parse
*/
deserialize?: (
str: string,
) => StorageValue<PersistedState> | Promise<StorageValue<PersistedState>>
/**
* Use a custom persist storage.
*
@ -197,180 +169,7 @@ const toThenable =
}
}
const oldImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
type S = ReturnType<typeof config>
let options = {
getStorage: () => localStorage,
serialize: JSON.stringify as (state: StorageValue<S>) => string,
deserialize: JSON.parse as (str: string) => StorageValue<S>,
partialize: (state: S) => state,
version: 0,
merge: (persistedState: unknown, currentState: S) => ({
...currentState,
...(persistedState as object),
}),
...baseOptions,
}
let hasHydrated = false
const hydrationListeners = new Set<PersistListener<S>>()
const finishHydrationListeners = new Set<PersistListener<S>>()
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<void> => {
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<S> & StorePersist<S, S>).persist = {
setOptions: (newOptions) => {
options = {
...options,
...newOptions,
}
if (newOptions.getStorage) {
storage = newOptions.getStorage()
}
},
clearStorage: () => {
storage?.removeItem(options.name)
},
getOptions: () => options,
rehydrate: () => hydrate() as Promise<void>,
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<typeof config>
let options = {
storage: createJSONStorage<S>(() => 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][] = [],

View File

@ -26,8 +26,6 @@ type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {
getServerState?: () => ExtractState<S>
}
let didWarnAboutEqualityFn = false
export function useStore<S extends WithReact<StoreApi<unknown>>>(
api: S,
): ExtractState<S>
@ -37,37 +35,15 @@ export function useStore<S extends WithReact<StoreApi<unknown>>, U>(
selector: (state: ExtractState<S>) => U,
): U
/**
* @deprecated Use `useStoreWithEqualityFn` from 'zustand/traditional'
* https://github.com/pmndrs/zustand/discussions/1937
*/
export function useStore<S extends WithReact<StoreApi<unknown>>, U>(
api: S,
selector: (state: ExtractState<S>) => U,
equalityFn: ((a: U, b: U) => boolean) | undefined,
): U
export function useStore<TState, StateSlice>(
api: WithReact<StoreApi<TState>>,
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<TState, StateSlice>(
export type UseBoundStore<S extends WithReact<ReadonlyStoreApi<unknown>>> = {
(): ExtractState<S>
<U>(selector: (state: ExtractState<S>) => U): U
/**
* @deprecated Use `createWithEqualityFn` from 'zustand/traditional'
*/
<U>(
selector: (state: ExtractState<S>) => U,
equalityFn: (a: U, b: U) => boolean,
): U
} & S
type Create = {
@ -92,26 +61,12 @@ type Create = {
<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
) => UseBoundStore<Mutate<StoreApi<T>, Mos>>
/**
* @deprecated Use `useStore` hook to bind store
*/
<S extends StoreApi<unknown>>(store: S): UseBoundStore<S>
}
const createImpl = <T>(createState: StateCreator<T, [], []>) => {
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)

View File

@ -9,10 +9,6 @@ export interface StoreApi<T> {
setState: SetStateInternal<T>
getState: () => T
subscribe: (listener: (state: T, prevState: T) => void) => () => void
/**
* @deprecated Use `unsubscribe` returned by `subscribe`
*/
destroy: () => void
}
type Get<T, K, F> = K extends keyof T ? T[K] : F
@ -88,82 +84,10 @@ const createStoreImpl: CreateStoreImpl = (createState) => {
return () => listeners.delete(listener)
}
const destroy: StoreApi<TState>['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<T> | ((s: T) => Partial<T>)` instead of `PartialState<T>`
*/
export type PartialState<T extends State> =
| Partial<T>
| ((state: T) => Partial<T>)
/**
* @deprecated Use `(s: T) => U` instead of `StateSelector<T, U>`
*/
export type StateSelector<T extends State, U> = (state: T) => U
/**
* @deprecated Use `(a: T, b: T) => boolean` instead of `EqualityChecker<T>`
*/
export type EqualityChecker<T> = (state: T, newState: T) => boolean
/**
* @deprecated Use `(state: T, previousState: T) => void` instead of `StateListener<T>`
*/
export type StateListener<T> = (state: T, previousState: T) => void
/**
* @deprecated Use `(slice: T, previousSlice: T) => void` instead of `StateSliceListener<T>`.
*/
export type StateSliceListener<T> = (slice: T, previousSlice: T) => void
/**
* @deprecated Use `(listener: (state: T) => void) => void` instead of `Subscribe<T>`.
*/
export type Subscribe<T extends State> = {
(listener: (state: T, previousState: T) => void): () => void
}
/**
* @deprecated You might be looking for `StateCreator`, if not then
* use `StoreApi<T>['setState']` instead of `SetState<T>`.
*/
export type SetState<T extends State> = {
_(
partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'],
replace?: boolean | undefined,
): void
}['_']
/**
* @deprecated You might be looking for `StateCreator`, if not then
* use `StoreApi<T>['getState']` instead of `GetState<T>`.
*/
export type GetState<T extends State> = () => T
/**
* @deprecated Use `StoreApi<T>['destroy']` instead of `Destroy`.
*/
export type Destroy = () => void

View File

@ -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<State>(() => ({ a: 0, b: 0 }))

View File

@ -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<StoreApi<CounterState>>()
const createStore = () =>
create<CounterState>((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}))
function Counter() {
const { count, inc } = useStore()
useEffect(inc, [inc])
return <div>count: {count * 1}</div>
}
const { findByText } = render(
<>
<Provider createStore={createStore}>
<Counter />
</Provider>
</>,
)
await findByText('count: 1')
})
it('uses context store with selectors', async () => {
const { Provider, useStore } = createContext<StoreApi<CounterState>>()
const createStore = () =>
create<CounterState>((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 <div>count: {count * 1}</div>
}
const { findByText } = render(
<>
<Provider createStore={createStore}>
<Counter />
</Provider>
</>,
)
await findByText('count: 1')
})
it('uses context store api', async () => {
const createStore = () =>
create<CounterState>()(
subscribeWithSelector((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
})),
)
type CustomStore = ReturnType<typeof createStore>
const { Provider, useStoreApi } = createContext<CustomStore>()
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 <div>count: {count * 1}</div>
}
const { findByText } = render(
<>
<Provider createStore={createStore}>
<Counter />
</Provider>
</>,
)
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 ? <div>errored</div> : this.props.children
}
}
const { useStore } = createContext<StoreApi<CounterState>>()
function Component() {
useStore()
return <div>no error</div>
}
const { findByText } = render(
<StrictMode>
<ErrorBoundary>
<Component />
</ErrorBoundary>
</StrictMode>,
)
await findByText('errored')
})
it('useCallback with useStore infers types correctly', async () => {
const { useStore } = createContext<StoreApi<CounterState>>()
function _Counter() {
const _x = useStore(useCallback((state) => state.count, []))
expectAreTypesEqual<typeof _x, number>().toBe(true)
}
})
const expectAreTypesEqual = <A, B>() => ({
toBe: (
_: (<T>() => T extends B ? 1 : 0) extends <T>() => T extends A ? 1 : 0
? true
: false,
) => {},
})

View File

@ -79,7 +79,6 @@ it('can use exposed types', () => {
_stateSelector: (state: ExampleState) => number,
_storeApi: StoreApi<ExampleState>,
_subscribe: StoreApi<ExampleState>['subscribe'],
_destroy: StoreApi<ExampleState>['destroy'],
_equalityFn: (a: ExampleState, b: ExampleState) => boolean,
_stateCreator: StateCreator<ExampleState>,
_useBoundStore: UseBoundStore<StoreApi<ExampleState>>,
@ -96,7 +95,6 @@ it('can use exposed types', () => {
selector,
storeApi,
storeApi.subscribe,
storeApi.destroy,
equalityFn,
stateCreator,
useBoundStore,

View File

@ -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)
})