mirror of
https://github.com/pmndrs/zustand.git
synced 2025-12-08 19:45:52 +00:00
[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:
parent
5c99468909
commit
6f99da1274
15
package.json
15
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",
|
||||
|
||||
100
src/context.ts
100
src/context.ts
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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][] = [],
|
||||
|
||||
49
src/react.ts
49
src/react.ts
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }))
|
||||
|
||||
@ -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,
|
||||
) => {},
|
||||
})
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user