Skip to content

feat(useStorageItem): allow to update state using previous value like… #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions src/storage/useStorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
51 changes: 33 additions & 18 deletions src/storage/useStorage.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,7 +14,7 @@ interface StorageResult extends AvailableResult {

type StorageItemResult<T> = [
T | undefined,
((value: T) => Promise<void>),
Dispatch<SetStateAction<T | undefined>>,
boolean
]

Expand Down Expand Up @@ -74,37 +74,52 @@ export function useStorageItem<T>(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<T>();

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
];
}