diff --git a/src/index.ts b/src/index.ts index 45d1871..33b8856 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,24 @@ import { AtomEffect } from 'recoil' +type PersistItemValue = string | undefined | null + export interface PersistStorage { - setItem(key: string, value: string): void | Promise - mergeItem?(key: string, value: string): Promise - getItem(key: string): null | string | Promise + setItem(key: string, value: string): any + mergeItem?(key: string, value: string): any + getItem(key: string): PersistItemValue | Promise } +export interface PersistState {} + export interface PersistConfiguration { - key?: string - storage?: PersistStorage + key: string + storage: PersistStorage +} + +interface PendingChanges { + queue: Promise | null + updates: Partial + reset: Record } /** @@ -19,7 +29,7 @@ export interface PersistConfiguration { * @param config.storage Local storage to use, defaults to `localStorage` */ export const recoilPersist = ( - config: PersistConfiguration = {}, + config: Partial = {}, ): { persistAtom: AtomEffect } => { if (typeof window === 'undefined') { return { @@ -27,76 +37,70 @@ export const recoilPersist = ( } } - const { key = 'recoil-persist', storage = localStorage } = config + const { + key = 'recoil-persist' as PersistConfiguration['key'], + storage = localStorage as PersistConfiguration['storage'], + } = config + + const pendingChanges: PendingChanges = { + queue: null, + updates: {}, + reset: {}, + } const persistAtom: AtomEffect = ({ onSet, node, trigger, setSelf }) => { if (trigger === 'get') { - const state = getState() - if (typeof state.then === 'function') { - state.then((s) => { - if (s.hasOwnProperty(node.key)) { - setSelf(s[node.key]) - } - }) - } - if (state.hasOwnProperty(node.key)) { - setSelf(state[node.key]) - } + getState().then((s) => { + if (s.hasOwnProperty(node.key)) { + setSelf(s[node.key]) + } + }) } - onSet(async (newValue, _, isReset) => { - const state = getState() - if (typeof state.then === 'function') { - state.then((s: any) => updateState(newValue, s, node.key, isReset)) + onSet((newValue, _, isReset) => { + if (isReset) { + pendingChanges.reset[node.key] = true + delete pendingChanges.updates[node.key] } else { - updateState(newValue, state, node.key, isReset) + pendingChanges.updates[node.key] = newValue + } + if (!pendingChanges.queue) { + pendingChanges.queue = getState().then((state) => { + updateState(state, pendingChanges) + pendingChanges.queue = null + pendingChanges.reset = {} + pendingChanges.updates = {} + }) } }) } - const updateState = ( - newValue: any, - state: any, - key: string, - isReset: boolean, - ) => { - if (isReset) { + const updateState = (state: PersistState, changes: PendingChanges) => { + Object.keys(changes.reset).forEach((key) => { delete state[key] - } else { - state[key] = newValue - } - + }) + Object.keys(changes.updates).forEach((key) => { + state[key] = changes.updates[key] + }) setState(state) } - const getState = (): any => { - const toParse = storage.getItem(key) + const parseState = (toParse: PersistItemValue): PersistState => { if (toParse === null || toParse === undefined) { return {} } - if (typeof toParse === 'string') { - return parseState(toParse) - } - if (typeof toParse.then === 'function') { - return toParse.then(parseState) - } - - return {} - } - - const parseState = (state: string) => { - if (state === undefined) { - return {} - } try { - return JSON.parse(state) + return JSON.parse(toParse) } catch (e) { console.error(e) return {} } } - const setState = (state: any): void => { + const getState = (): Promise => + Promise.resolve(storage.getItem(key)).then(parseState) + + const setState = (state: PersistState): void => { try { if (typeof storage.mergeItem === 'function') { storage.mergeItem(key, JSON.stringify(state)) diff --git a/test/index.spec.tsx b/test/index.spec.tsx index a7b7bcb..8318b97 100644 --- a/test/index.spec.tsx +++ b/test/index.spec.tsx @@ -25,7 +25,7 @@ const asyncStorage = (): TestableStorage => { setItem: (key: string, value: string) => { return new Promise((resolve) => { s[key] = value - resolve() + resolve(undefined) }) }, getItem: (key: string): Promise => { @@ -103,7 +103,8 @@ function testPersistWith(storage: TestableStorage) { const updateMultiple = useRecoilCallback(({ set }) => () => { set(counterState, 10) set(counterFamily('2'), 10) - }) + }, []); + return (

{count}

@@ -382,7 +383,7 @@ function testPersistWith(storage: TestableStorage) { ) }) - it.skip('should handle updating multiple atomes', async () => { + it('should handle updating multiple atoms', async () => { const { getByTestId } = render(