diff --git a/.size-snapshot.json b/.size-snapshot.json index 0da8f5f966..6c49ad5536 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -14,16 +14,16 @@ } }, "utils.js": { - "bundled": 13354, - "minified": 6528, - "gzipped": 2437, + "bundled": 13604, + "minified": 6640, + "gzipped": 2497, "treeshaked": { "rollup": { "code": 28, "import_statements": 28 }, "webpack": { - "code": 1280 + "code": 1305 } } }, diff --git a/src/utils.ts b/src/utils.ts index 419b007cc0..980739f2c8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -12,4 +12,8 @@ export { freezeAtom, freezeAtomCreator } from './utils/freezeAtom' export { splitAtom } from './utils/splitAtom' export { atomWithDefault } from './utils/atomWithDefault' export { waitForAll } from './utils/waitForAll' -export { atomWithStorage, atomWithHash } from './utils/atomWithStorage' +export { + atomWithStorage, + atomWithHash, + createJSONStorage, +} from './utils/atomWithStorage' diff --git a/src/utils/atomWithStorage.ts b/src/utils/atomWithStorage.ts index 437a2b1526..b7aff4b353 100644 --- a/src/utils/atomWithStorage.ts +++ b/src/utils/atomWithStorage.ts @@ -10,18 +10,27 @@ type Storage = { subscribe?: (key: string, callback: (value: Value) => void) => Unsubscribe } -const defaultStorage: Storage = { +type StringStorage = { + getItem: (key: string) => string | null | Promise + setItem: (key: string, newValue: string) => void | Promise +} + +export const createJSONStorage = ( + getStringStorage: () => StringStorage +): Storage => ({ getItem: (key) => { - const storedValue = localStorage.getItem(key) - if (storedValue === null) { - throw new Error('no value stored') + const value = getStringStorage().getItem(key) + if (value instanceof Promise) { + return value.then((v) => JSON.parse(v || '')) } - return JSON.parse(storedValue) + return JSON.parse(value || '') }, setItem: (key, newValue) => { - localStorage.setItem(key, JSON.stringify(newValue)) + getStringStorage().setItem(key, JSON.stringify(newValue)) }, -} +}) + +const defaultStorage = createJSONStorage(() => localStorage) export function atomWithStorage( key: string, @@ -30,7 +39,11 @@ export function atomWithStorage( ): PrimitiveAtom { const getInitialValue = () => { try { - return storage.getItem(key) + const value = storage.getItem(key) + if (value instanceof Promise) { + return value.catch(() => initialValue) + } + return value } catch { return initialValue } diff --git a/tests/utils/atomWithStorage.test.tsx b/tests/utils/atomWithStorage.test.tsx index ee1d1f94c5..987f1a62b1 100644 --- a/tests/utils/atomWithStorage.test.tsx +++ b/tests/utils/atomWithStorage.test.tsx @@ -6,7 +6,7 @@ import { getTestProvider } from '../testUtils' const Provider = getTestProvider() -describe('atomWithStorage', () => { +describe('atomWithStorage (sync)', () => { const storageData: Record = { count: 10, } @@ -47,7 +47,9 @@ describe('atomWithStorage', () => { await findByText('count: 11') expect(storageData.count).toBe(11) }) +}) +describe('atomWithStorage (async)', () => { const asyncStorageData: Record = { count: 10, } @@ -86,11 +88,73 @@ describe('atomWithStorage', () => { ) + await findByText('loading') await findByText('count: 10') fireEvent.click(getByText('button')) await findByText('count: 11') - expect(storageData.count).toBe(11) + await new Promise((r) => setTimeout(r, 20)) + expect(asyncStorageData.count).toBe(11) + }) + + it('async new count', async () => { + const countAtom = atomWithStorage('count2', 20, asyncDummyStorage) + + const Counter: React.FC = () => { + const [count, setCount] = useAtom(countAtom) + return ( + <> +
count: {count}
+ + + ) + } + + const { findByText, getByText } = render( + + + + + + ) + + await findByText('loading') + await findByText('count: 20') + + fireEvent.click(getByText('button')) + await findByText('count: 21') + await new Promise((r) => setTimeout(r, 20)) + expect(asyncStorageData.count2).toBe(21) + }) + + it('async new count with delayInit', async () => { + const countAtom = atomWithStorage('count3', 30, { + ...asyncDummyStorage, + delayInit: true, + }) + + const Counter: React.FC = () => { + const [count, setCount] = useAtom(countAtom) + return ( + <> +
count: {count}
+ + + ) + } + + const { findByText, getByText } = render( + + + + ) + + await findByText('count: 30') + + fireEvent.click(getByText('button')) + await findByText('count: 31') + await new Promise((r) => setTimeout(r, 20)) + expect(asyncStorageData.count3).toBe(31) }) })