Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(persist): improve ssr & add versions & new callback #275

Merged
merged 1 commit into from
Jan 8, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 85 additions & 29 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,51 @@ type StateStorage = {
getItem: (name: string) => string | null | Promise<string | null>
setItem: (name: string, value: string) => void | Promise<void>
}
type StorageValue<S> = { state: S; version: number }
type PersistOptions<S> = {
/** Name of the storage (must be unique) */
name: string
storage?: StateStorage
serialize?: (state: S) => string | Promise<string>
deserialize?: (str: string) => S | Promise<S>
/**
* 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
/**
* Use a custom serializer.
* The returned string will be stored in the storage.
*
* @default JSON.stringify
*/
serialize?: (state: StorageValue<S>) => string | Promise<string>
/**
* Use a custom deserializer.
*
* @param str The storage's current value.
* @default JSON.parse
*/
deserialize?: (str: string) => StorageValue<S> | Promise<S>
/**
* Prevent some items from being stored.
*/
blacklist?: (keyof S)[]
/**
* Only store the listed properties.
*/
whitelist?: (keyof S)[]
postRehydrationMiddleware?: () => void
/**
* A function returning another (optional) function.
* The main function will be called before the storage rehydration.
* The returned function will be called after the storage rehydration.
*/
onRehydrateStorage?: (state: S) => ((state: S) => void) | void
/**
* If the stored state's version mismatch the one specified here, the storage will not be used.
* This is useful when adding a breaking change to your store.
*/
version?: number
}

export const persist = <S extends State>(
Expand All @@ -137,30 +174,38 @@ export const persist = <S extends State>(
) => (set: SetState<S>, get: GetState<S>, api: StoreApi<S>): S => {
const {
name,
storage = typeof localStorage !== 'undefined'
? localStorage
: {
getItem: () => null,
setItem: () => {},
},
getStorage = () => localStorage,
serialize = JSON.stringify,
deserialize = JSON.parse,
blacklist,
whitelist,
postRehydrationMiddleware,
onRehydrateStorage,
version = 0,
} = options || {}

let storage: StateStorage | undefined

try {
storage = getStorage()
} catch (e) {
// prevent error if the storage is not defined (e.g. when server side rendering a page)
}

if (!storage) return config(set, get, api)

const setItem = async () => {
const state = { ...get() }

if (whitelist) {
Object.keys(state).forEach(
(key) => !whitelist.includes(key) && delete state[key]
)
Object.keys(state).forEach((key) => {
!whitelist.includes(key) && delete state[key]
})
}
if (blacklist) {
blacklist.forEach((key) => delete state[key])
}
storage.setItem(name, await serialize(state))

storage?.setItem(name, await serialize({ state, version }))
AnatoleLucet marked this conversation as resolved.
Show resolved Hide resolved
}

const savedSetState = api.setState
Expand All @@ -170,25 +215,36 @@ export const persist = <S extends State>(
setItem()
}

const state = config(
(payload) => {
set(payload)
setItem()
},
get,
api
)

// rehydrate initial state with existing stored state
;(async () => {
const postRehydrationCallback = onRehydrateStorage?.(get()) || undefined

try {
const storedState = await storage.getItem(name)
if (storedState) set(await deserialize(storedState))
const storageValue = await storage.getItem(name)

if (storageValue) {
const deserializedStorageValue = await deserialize(storageValue)

// if versions mismatch, clear storage by storing the new initial state
if (deserializedStorageValue.version !== version) {
setItem()
} else {
set(deserializedStorageValue.state)
}
}
} catch (e) {
console.error(new Error(`Unable to get to stored state in "${name}"`))
throw new Error(`Unable to get to stored state in "${name}"`)
} finally {
postRehydrationMiddleware?.()
postRehydrationCallback?.(get())
}
})()

return state
return config(
(payload) => {
set(payload)
setItem()
},
get,
api
)
}