mirror of
https://github.com/pmndrs/zustand.git
synced 2025-12-08 19:45:52 +00:00
feat(middleare/persist): return storage promise from setState (#3206)
* feat(middleare/persist): return storage promise from setState * refactor types (technically breaking) * another breaking change in types * make public types not breaking
This commit is contained in:
parent
2cc19881fa
commit
feddc0c210
@ -4,10 +4,10 @@ import type {
|
|||||||
StoreMutatorIdentifier,
|
StoreMutatorIdentifier,
|
||||||
} from '../vanilla.ts'
|
} from '../vanilla.ts'
|
||||||
|
|
||||||
export interface StateStorage {
|
export interface StateStorage<R = unknown> {
|
||||||
getItem: (name: string) => string | null | Promise<string | null>
|
getItem: (name: string) => string | null | Promise<string | null>
|
||||||
setItem: (name: string, value: string) => unknown | Promise<unknown>
|
setItem: (name: string, value: string) => R
|
||||||
removeItem: (name: string) => unknown | Promise<unknown>
|
removeItem: (name: string) => R
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StorageValue<S> = {
|
export type StorageValue<S> = {
|
||||||
@ -15,12 +15,12 @@ export type StorageValue<S> = {
|
|||||||
version?: number
|
version?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PersistStorage<S> {
|
export interface PersistStorage<S, R = unknown> {
|
||||||
getItem: (
|
getItem: (
|
||||||
name: string,
|
name: string,
|
||||||
) => StorageValue<S> | null | Promise<StorageValue<S> | null>
|
) => StorageValue<S> | null | Promise<StorageValue<S> | null>
|
||||||
setItem: (name: string, value: StorageValue<S>) => unknown | Promise<unknown>
|
setItem: (name: string, value: StorageValue<S>) => R
|
||||||
removeItem: (name: string) => unknown | Promise<unknown>
|
removeItem: (name: string) => R
|
||||||
}
|
}
|
||||||
|
|
||||||
type JsonStorageOptions = {
|
type JsonStorageOptions = {
|
||||||
@ -28,18 +28,18 @@ type JsonStorageOptions = {
|
|||||||
replacer?: (key: string, value: unknown) => unknown
|
replacer?: (key: string, value: unknown) => unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createJSONStorage<S>(
|
export function createJSONStorage<S, R = unknown>(
|
||||||
getStorage: () => StateStorage,
|
getStorage: () => StateStorage<R>,
|
||||||
options?: JsonStorageOptions,
|
options?: JsonStorageOptions,
|
||||||
): PersistStorage<S> | undefined {
|
): PersistStorage<S, unknown> | undefined {
|
||||||
let storage: StateStorage | undefined
|
let storage: StateStorage<R> | undefined
|
||||||
try {
|
try {
|
||||||
storage = getStorage()
|
storage = getStorage()
|
||||||
} catch {
|
} catch {
|
||||||
// prevent error if the storage is not defined (e.g. when server side rendering a page)
|
// prevent error if the storage is not defined (e.g. when server side rendering a page)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const persistStorage: PersistStorage<S> = {
|
const persistStorage: PersistStorage<S, R> = {
|
||||||
getItem: (name) => {
|
getItem: (name) => {
|
||||||
const parse = (str: string | null) => {
|
const parse = (str: string | null) => {
|
||||||
if (str === null) {
|
if (str === null) {
|
||||||
@ -60,7 +60,11 @@ export function createJSONStorage<S>(
|
|||||||
return persistStorage
|
return persistStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PersistOptions<S, PersistedState = S> {
|
export interface PersistOptions<
|
||||||
|
S,
|
||||||
|
PersistedState = S,
|
||||||
|
PersistReturn = unknown,
|
||||||
|
> {
|
||||||
/** Name of the storage (must be unique) */
|
/** Name of the storage (must be unique) */
|
||||||
name: string
|
name: string
|
||||||
/**
|
/**
|
||||||
@ -71,7 +75,7 @@ export interface PersistOptions<S, PersistedState = S> {
|
|||||||
*
|
*
|
||||||
* @default createJSONStorage(() => localStorage)
|
* @default createJSONStorage(() => localStorage)
|
||||||
*/
|
*/
|
||||||
storage?: PersistStorage<PersistedState> | undefined
|
storage?: PersistStorage<PersistedState, PersistReturn> | undefined
|
||||||
/**
|
/**
|
||||||
* Filter the persisted value.
|
* Filter the persisted value.
|
||||||
*
|
*
|
||||||
@ -118,17 +122,28 @@ export interface PersistOptions<S, PersistedState = S> {
|
|||||||
|
|
||||||
type PersistListener<S> = (state: S) => void
|
type PersistListener<S> = (state: S) => void
|
||||||
|
|
||||||
type StorePersist<S, Ps> = {
|
type StorePersist<S, Ps, Pr> = S extends {
|
||||||
persist: {
|
getState: () => infer T
|
||||||
setOptions: (options: Partial<PersistOptions<S, Ps>>) => void
|
setState: {
|
||||||
clearStorage: () => void
|
// capture both overloads of setState
|
||||||
rehydrate: () => Promise<void> | void
|
(...args: infer Sa1): infer Sr1
|
||||||
hasHydrated: () => boolean
|
(...args: infer Sa2): infer Sr2
|
||||||
onHydrate: (fn: PersistListener<S>) => () => void
|
|
||||||
onFinishHydration: (fn: PersistListener<S>) => () => void
|
|
||||||
getOptions: () => Partial<PersistOptions<S, Ps>>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
? {
|
||||||
|
setState(...args: Sa1): Sr1 | Pr
|
||||||
|
setState(...args: Sa2): Sr2 | Pr
|
||||||
|
persist: {
|
||||||
|
setOptions: (options: Partial<PersistOptions<T, Ps, Pr>>) => void
|
||||||
|
clearStorage: () => void
|
||||||
|
rehydrate: () => Promise<void> | void
|
||||||
|
hasHydrated: () => boolean
|
||||||
|
onHydrate: (fn: PersistListener<T>) => () => void
|
||||||
|
onFinishHydration: (fn: PersistListener<T>) => () => void
|
||||||
|
getOptions: () => Partial<PersistOptions<T, Ps, Pr>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: never
|
||||||
|
|
||||||
type Thenable<Value> = {
|
type Thenable<Value> = {
|
||||||
then<V>(
|
then<V>(
|
||||||
@ -172,7 +187,7 @@ const toThenable =
|
|||||||
const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
|
const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
|
||||||
type S = ReturnType<typeof config>
|
type S = ReturnType<typeof config>
|
||||||
let options = {
|
let options = {
|
||||||
storage: createJSONStorage<S>(() => localStorage),
|
storage: createJSONStorage<S, void>(() => localStorage),
|
||||||
partialize: (state: S) => state,
|
partialize: (state: S) => state,
|
||||||
version: 0,
|
version: 0,
|
||||||
merge: (persistedState: unknown, currentState: S) => ({
|
merge: (persistedState: unknown, currentState: S) => ({
|
||||||
@ -202,7 +217,7 @@ const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
|
|||||||
|
|
||||||
const setItem = () => {
|
const setItem = () => {
|
||||||
const state = options.partialize({ ...get() })
|
const state = options.partialize({ ...get() })
|
||||||
return (storage as PersistStorage<S>).setItem(options.name, {
|
return (storage as PersistStorage<S, unknown>).setItem(options.name, {
|
||||||
state,
|
state,
|
||||||
version: options.version,
|
version: options.version,
|
||||||
})
|
})
|
||||||
@ -212,13 +227,13 @@ const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
|
|||||||
|
|
||||||
api.setState = (state, replace) => {
|
api.setState = (state, replace) => {
|
||||||
savedSetState(state, replace as any)
|
savedSetState(state, replace as any)
|
||||||
void setItem()
|
return setItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
const configResult = config(
|
const configResult = config(
|
||||||
(...args) => {
|
(...args) => {
|
||||||
set(...(args as Parameters<typeof set>))
|
set(...(args as Parameters<typeof set>))
|
||||||
void setItem()
|
return setItem()
|
||||||
},
|
},
|
||||||
get,
|
get,
|
||||||
api,
|
api,
|
||||||
@ -307,7 +322,7 @@ const persistImpl: PersistImpl = (config, baseOptions) => (set, get, api) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
;(api as StoreApi<S> & StorePersist<S, S>).persist = {
|
;(api as StoreApi<S> & StorePersist<StoreApi<S>, S, unknown>).persist = {
|
||||||
setOptions: (newOptions) => {
|
setOptions: (newOptions) => {
|
||||||
options = {
|
options = {
|
||||||
...options,
|
...options,
|
||||||
@ -365,9 +380,7 @@ declare module '../vanilla' {
|
|||||||
|
|
||||||
type Write<T, U> = Omit<T, keyof U> & U
|
type Write<T, U> = Omit<T, keyof U> & U
|
||||||
|
|
||||||
type WithPersist<S, A> = S extends { getState: () => infer T }
|
type WithPersist<S, A> = Write<S, StorePersist<S, A, unknown>>
|
||||||
? Write<S, StorePersist<T, A>>
|
|
||||||
: never
|
|
||||||
|
|
||||||
type PersistImpl = <T>(
|
type PersistImpl = <T>(
|
||||||
storeInitializer: StateCreator<T, [], []>,
|
storeInitializer: StateCreator<T, [], []>,
|
||||||
|
|||||||
@ -165,7 +165,9 @@ describe('persist middleware with async configuration', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Write something to the store
|
// Write something to the store
|
||||||
act(() => useBoundStore.setState({ count: 42 }))
|
act(() => {
|
||||||
|
useBoundStore.setState({ count: 42 })
|
||||||
|
})
|
||||||
expect(await screen.findByText('count: 42')).toBeInTheDocument()
|
expect(await screen.findByText('count: 42')).toBeInTheDocument()
|
||||||
expect(setItemSpy).toBeCalledWith(
|
expect(setItemSpy).toBeCalledWith(
|
||||||
'test-storage',
|
'test-storage',
|
||||||
@ -788,7 +790,9 @@ describe('persist middleware with async configuration', () => {
|
|||||||
|
|
||||||
// Write something to the store
|
// Write something to the store
|
||||||
const updatedMap = new Map(map).set('foo', 'bar')
|
const updatedMap = new Map(map).set('foo', 'bar')
|
||||||
act(() => useBoundStore.setState({ map: updatedMap }))
|
act(() => {
|
||||||
|
useBoundStore.setState({ map: updatedMap })
|
||||||
|
})
|
||||||
expect(await screen.findByText('map-content: bar')).toBeInTheDocument()
|
expect(await screen.findByText('map-content: bar')).toBeInTheDocument()
|
||||||
|
|
||||||
expect(setItemSpy).toBeCalledWith(
|
expect(setItemSpy).toBeCalledWith(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user