From 9a2ac5a61ccb700f68d0294f613a0df4637e88a7 Mon Sep 17 00:00:00 2001 From: mustapha Date: Sat, 18 Apr 2020 11:49:11 +0100 Subject: [PATCH 1/3] feat(useStorageItem): allow to update state using previous value like React setState --- src/storage/useStorage.test.ts | 38 +++++++++++++++++++++++++++++++++- src/storage/useStorage.ts | 34 +++++++++++++++++------------- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/storage/useStorage.test.ts b/src/storage/useStorage.test.ts index 148a7f9..9c13bf6 100644 --- a/src/storage/useStorage.test.ts +++ b/src/storage/useStorage.test.ts @@ -116,4 +116,40 @@ it('Manages individual item with stored value', async () => { setValue('Frank'); }); -}); \ No newline at end of file +}); + +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'); + }); +}); diff --git a/src/storage/useStorage.ts b/src/storage/useStorage.ts index c3d6d72..a764efa 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 } 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,6 +74,7 @@ export function useStorageItem(key: string, initialValue?: T): StorageItemRes ]; } + const [ready, setReady] = useState(false) const [storedValue, setStoredValue] = useState(); useEffect(() => { @@ -82,29 +83,34 @@ export function useStorageItem(key: string, initialValue?: T): StorageItemRes 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); + setStoredValue(result.value as any); } else { setStoredValue(typeof result.value === 'string' ? result.value : JSON.parse(result.value!)); } + setReady(true); } catch (e) { return initialValue; } } 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, initialValue, key]); + + 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 ]; } From 6a43d98fc539ee19c8a5f6e677a64a90b1b60acc Mon Sep 17 00:00:00 2001 From: mustapha Date: Tue, 21 Apr 2020 12:22:01 +0000 Subject: [PATCH 2/3] fix(useStorageItem): prevent loop reloading --- src/storage/useStorage.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/storage/useStorage.ts b/src/storage/useStorage.ts index a764efa..e0bcf4f 100644 --- a/src/storage/useStorage.ts +++ b/src/storage/useStorage.ts @@ -78,6 +78,8 @@ export function useStorageItem(key: string, initialValue?: T): StorageItemRes const [storedValue, setStoredValue] = useState(); useEffect(() => { + if(ready) return; + async function loadValue() { try { const result = await Storage.get({ key }); @@ -93,7 +95,7 @@ export function useStorageItem(key: string, initialValue?: T): StorageItemRes } } loadValue(); - }, [Storage, initialValue, key]); + }, [Storage, initialValue, key, ready]); useEffect(() => { if(!ready) return; From 603eeefec6ba43fd8ec5faeb802d07a262f4bf2c Mon Sep 17 00:00:00 2001 From: mustapha Date: Mon, 18 May 2020 19:24:07 +0000 Subject: [PATCH 3/3] fix(useStorageItem): return correct type --- src/storage/useStorage.test.ts | 153 +++++++++++++++++++++++++++++++++ src/storage/useStorage.ts | 21 +++-- 2 files changed, 167 insertions(+), 7 deletions(-) diff --git a/src/storage/useStorage.test.ts b/src/storage/useStorage.test.ts index 9c13bf6..eefbabc 100644 --- a/src/storage/useStorage.test.ts +++ b/src/storage/useStorage.test.ts @@ -153,3 +153,156 @@ it('Sets storage value using previous value', async () => { 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 e0bcf4f..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, Dispatch, SetStateAction } 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'; @@ -74,6 +74,9 @@ 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(); @@ -81,21 +84,25 @@ export function useStorageItem(key: string, initialValue?: T): StorageItemRes 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); - setStoredValue(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, initialValue, key, ready]); + }, [Storage, key, ready]); useEffect(() => { if(!ready) return;