Skip to content

Commit

Permalink
feat(persist): improve ssr & add versions & new callback
Browse files Browse the repository at this point in the history
- add js doc for option object
- improve ssr support
- add a new version option #243
- refactor postRehydrationMiddleware to onRehydrateStorage
  • Loading branch information
AnatoleLucet committed Jan 6, 2021
1 parent 58c286e commit 952b16f
Showing 1 changed file with 92 additions and 27 deletions.
119 changes: 92 additions & 27 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,52 @@ type StateStorage = {
setItem: (name: string, value: string) => void | Promise<void>
}
type PersistOptions<S> = {
/** Name of the storage (must be unique) */
name: string
storage?: StateStorage
/**
* A function returning a storage.
* The storage must fit `window.localStorage`'s api.
* For example the storage could be `AsyncStorage` from React Native.
*
* @default () => localStorage
*/
storage?: () => StateStorage
/**
* Use a custom serializer.
* The returned string will be stored in the storage.
*
* @default JSON.stringify
*/
serialize?: (state: S) => string | Promise<string>
/**
* Use a custom deserializer.
* The returned value will be used as the new state.
*
* @param str The storage's current value.
* @default JSON.parse
*/
deserialize?: (str: string) => S | Promise<S>
/**
* Prevent some items from being stored.
* Nested properties can be accessed like so: `foo.bar.baz`
*/
blacklist?: (keyof S)[]
/**
* Only store the listed properties.
* Nested properties can be accessed like so: `foo.bar.baz`
*/
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 you store.
*/
version?: number
}

export const persist = <S extends State>(
Expand All @@ -137,30 +176,45 @@ 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: () => {},
},
storage: getStorage = () => localStorage,
serialize = JSON.stringify,
deserialize = JSON.parse,
blacklist,
whitelist,
postRehydrationMiddleware,
onRehydrateStorage,
version,
} = 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() }
let 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))

if (version) {
state = {
...state,
__version: version,
}
}

storage?.setItem(name, await serialize(state))
}

const savedSetState = api.setState
Expand All @@ -170,25 +224,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))

if (storedState) {
const deserializedState = await deserialize(storedState)

// if versions mismatch, clear storage by storing the new initial state
if (deserializedState.__version !== version) {
setItem()
} else {
set(deserializedState)
}
}
} 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
)
}

0 comments on commit 952b16f

Please sign in to comment.