diff --git a/src/storage/useStorage.test.ts b/src/storage/useStorage.test.ts index 148a7f9..eefbabc 100644 --- a/src/storage/useStorage.test.ts +++ b/src/storage/useStorage.test.ts @@ -116,4 +116,193 @@ it('Manages individual item with stored value', async () => { setValue('Frank'); }); +}); + +it('Sets storage value using previous value', async () => { + let r: any; + const storageMock = (Plugins.Storage as any); + await act(async () => { + storageMock.__init({ name: 'Max'}); + }); + + await act(async () => { + r = renderHook(() => useStorageItem('name', '')); + }); + + await act(async () => { + }); + + await act(async () => { + const result = r.result.current; + + const [value, setValue] = result; + expect(value).toBe('Max'); + + setValue((name: string) => name.toUpperCase()); + }); + + await act(async () => { + const result = r.result.current; + + const [value, setValue] = result; + expect(value).toBe('MAX'); + }); + + await act(async () => { + const storedValue = await storageMock.get({key: 'name'}); + expect(storedValue.value).toBe('MAX'); + }); +}); + + +it('Must have correct type on initialization', async () => { + let storeNumber: any; + let storeBoolean: any; + let storeArray: any; + let storeObject: any; + let storeUndefined: any; + const storageMock = (Plugins.Storage as any); + await act(async () => { + storageMock.__init({}); + }); + + await act(async () => { + storeNumber = renderHook(() => useStorageItem('num', 0)); + storeBoolean = renderHook(() => useStorageItem('bool', true)); + storeArray = renderHook(() => useStorageItem('array', [])); + storeObject = renderHook(() => useStorageItem('obj', {})); + storeUndefined = renderHook(() => useStorageItem('und')); + }); + + await act(async () => { + const result = storeBoolean.result.current; + const [value, setValue] = result; + expect(typeof value).toBe("boolean"); + expect(value).toBe(true); + }); + await act(async () => { + const result = storeNumber.result.current; + const [value, setValue] = result; + expect(typeof value).toBe("number"); + expect(value).toBe(0); + }); + await act(async () => { + const result = storeArray.result.current; + const [value, setValue] = result; + expect(typeof value).toBe("object"); + expect(Array.isArray(value)).toBe(true); + expect(JSON.stringify(value)).toBe("[]"); + }); + await act(async () => { + const result = storeObject.result.current; + const [value, setValue] = result; + expect(typeof value).toBe("object"); + expect(JSON.stringify(value)).toBe("{}"); + }); + + await act(async () => { + const result = storeUndefined.result.current; + const [value, setValue] = result; + expect(typeof value).toBe("undefined"); + expect(JSON.stringify(value)).toBe(undefined); + }); + + await act(async () => { + const storedValue = await storageMock.get({key: 'num'}); + expect(storedValue.value).toBe("0"); + }); + + await act(async () => { + const storedValue = await storageMock.get({key: 'bool'}); + expect(storedValue.value).toBe("true"); + }); + + await act(async () => { + const storedValue = await storageMock.get({key: 'array'}); + expect(storedValue.value).toBe("[]"); + }); + + await act(async () => { + const storedValue = await storageMock.get({key: 'obj'}); + expect(storedValue.value).toBe("{}"); + }); + + await act(async () => { + const storedValue = await storageMock.get({key: 'und'}); + expect(storedValue.value).toBe(undefined); + }); +}); + + +it('Must have correct type when already initiated', async () => { + let storeNumber: any; + let storeBoolean: any; + let storeArray: any; + let storeObject: any; + let storeUndefined: any; + const storageMock = (Plugins.Storage as any); + await act(async () => { + storageMock.__init({num:'0', bool: 'true', arr: "[]", obj: "{}", und:'undefined'}); + }); + + await act(async () => { + storeNumber = renderHook(() => useStorageItem('num')); + storeBoolean = renderHook(() => useStorageItem('bool')); + storeArray = renderHook(() => useStorageItem('arr')); + storeObject = renderHook(() => useStorageItem('obj')); + storeUndefined = renderHook(() => useStorageItem('und')); + }); + + await act(async () => { + const result = storeBoolean.result.current; + const [value, setValue] = result; + expect(typeof value).toBe("boolean"); + expect(value).toBe(true); + }); + await act(async () => { + const result = storeNumber.result.current; + const [value, setValue] = result; + expect(typeof value).toBe("number"); + expect(value).toBe(0); + }); + await act(async () => { + const result = storeArray.result.current; + const [value, setValue] = result; + expect(typeof value).toBe("object"); + expect(Array.isArray(value)).toBe(true); + expect(JSON.stringify(value)).toBe("[]"); + }); + await act(async () => { + const result = storeObject.result.current; + const [value, setValue] = result; + expect(typeof value).toBe("object"); + expect(JSON.stringify(value)).toBe("{}"); + }); + await act(async () => { + const result = storeUndefined.result.current; + const [value, setValue] = result; + expect(typeof value).toBe("undefined"); + }); + + await act(async () => { + const storedValue = await storageMock.get({key: 'num'}); + expect(storedValue.value).toBe("0"); + }); + + await act(async () => { + const storedValue = await storageMock.get({key: 'bool'}); + expect(storedValue.value).toBe("true"); + }); + await act(async () => { + const storedValue = await storageMock.get({key: 'arr'}); + expect(storedValue.value).toBe("[]"); + }); + await act(async () => { + const storedValue = await storageMock.get({key: 'obj'}); + expect(storedValue.value).toBe("{}"); + }); + await act(async () => { + const storedValue = await storageMock.get({key: 'und'}); + expect(storedValue.value).toBe("undefined"); + }); }); \ No newline at end of file diff --git a/src/storage/useStorage.ts b/src/storage/useStorage.ts index c3d6d72..b5f25a3 100644 --- a/src/storage/useStorage.ts +++ b/src/storage/useStorage.ts @@ -1,5 +1,5 @@ // Inspired by useLocalStorage from https://usehooks.com/useLocalStorage/ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, Dispatch, SetStateAction, useRef } from 'react'; import { Plugins } from '@capacitor/core'; import { AvailableResult, notAvailable } from '../util/models'; import { isFeatureAvailable, featureNotAvailableError } from '../util/feature-check'; @@ -14,7 +14,7 @@ interface StorageResult extends AvailableResult { type StorageItemResult = [ T | undefined, - ((value: T) => Promise), + Dispatch>, boolean ] @@ -74,37 +74,52 @@ export function useStorageItem(key: string, initialValue?: T): StorageItemRes ]; } + // We don't want to rerender when initialValue changes so we use ref + const initialValueRef = useRef(initialValue) + + const [ready, setReady] = useState(false) const [storedValue, setStoredValue] = useState(); useEffect(() => { + if(ready) return; + async function loadValue() { + const initialValue = initialValueRef.current; try { const result = await Storage.get({ key }); - if (result.value == undefined && initialValue != undefined) { - result.value = typeof initialValue === "string" ? initialValue : JSON.stringify(initialValue); - setValue(result.value as any); + if (result.value == undefined && initialValue == undefined) return + + if (result.value == undefined) { + setStoredValue(initialValue); } else { - setStoredValue(typeof result.value === 'string' ? result.value : JSON.parse(result.value!)); + setStoredValue(typeof initialValue === 'string' ? result.value : JSON.parse(result.value!)); } + setReady(true); } catch (e) { - return initialValue; + // We might have some parse errors + setReady(true); + return; } } loadValue(); - }, [Storage, setStoredValue, initialValue, key]); - - const setValue = async (value: T) => { - try { - setStoredValue(value); - await Storage.set({ key, value: typeof value === "string" ? value : JSON.stringify(value) }); - } catch (e) { - console.error(e); - } - } + }, [Storage, key, ready]); + + useEffect(() => { + if(!ready) return; + + async function updateValue() { + try { + await Storage.set({ key, value: typeof storedValue === "string" ? storedValue : JSON.stringify(storedValue) }); + } catch (e) { + console.error(e); + } + } + updateValue(); + }, [ready, storedValue]) return [ storedValue, - setValue, + setStoredValue, true ]; }