import localforage from 'localforage'; import { useCallback, useEffect, useState } from 'react'; type Setter = (value: any) => Promise; /** * Usage example: * * const IS_DEV_KEY = 'is-dev'; * * const { values: [isDev], setItem } = useLocalforage<[boolean]>([IS_DEV_KEY]); */ export function useLocalforage>( keys: string[], defaults = [] as any[], options: LocalForageOptions = { name: 'arcus' } ) { const channelId = `localforage-${options.storeName ?? 'arcus'}-${options.name ?? 'default'}`; const [store, setStore] = useState(null); const [values, setValues] = useState(defaults as unknown as T); const [setters, setSetters] = useState([] as Setter[]); const sendKeyValueToChannel = useCallback( ([key, value]: [string, any]) => { const channel = getChannel(channelId, 'send'); channel.postMessage({ key, value }); }, [channelId] ); const refresh = useCallback(async () => { if (store) { const values = await Promise.all( keys.map((key, index) => store.getItem(key).then((value) => { if (value === null) { value = defaults[index]; } return value; }) ) ); const setters = keys.map((key, index) => async (value: any | Function) => { const defaultValue = defaults[index]; if (typeof value === 'function') { value = value((await store.getItem(key)) || defaultValue); } else { value = value === undefined ? defaultValue : value; } await store.setItem(key, value); sendKeyValueToChannel([key, value]); }); setValues(values as T); setSetters(setters); return values; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [keys.join('/'), !!store]); const setItem = useCallback( async (key: string, value: any) => { if (store) { const result = await store.setItem(key, value); sendKeyValueToChannel([key, value]); return result; } }, [sendKeyValueToChannel, store] ); useEffect(() => (refresh(), undefined), [refresh]); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect( () => (!store && setStore(localforage.createInstance(options)), undefined), [options] // eslint-disable-line react-hooks/exhaustive-deps ); useEffect(() => { const channel = getChannel(channelId, 'receive'); function listener({ data }: any) { if (keys.includes(data.key)) { const { key, value } = data; setValues((values) => { const index = keys.indexOf(key); if (index > -1) { values[index] = value; } return structuredClone(values) as T; }); } } channel.addEventListener('message', listener); return () => { channel.removeEventListener('message', listener); /** * Leave channel open, or figure out a way to handle it as a singleton. */ // closeChannel(channelId, 'send'); // closeChannel(channelId, 'receive'); }; }, [setValues, keys, channelId]); return { values, refresh, setters, setItem, store }; } const CHANNELS = new Map(); function getChannel(channelId: string, sendOrReceive: 'send' | 'receive') { const id = `${channelId}-${sendOrReceive}`; if (!CHANNELS.has(id)) { CHANNELS.set(id, new BroadcastChannel(channelId)); } return CHANNELS.get(id) as BroadcastChannel; } function closeChannel(channelId: string, sendOrReceive: 'send' | 'receive') { const id = `${channelId}-${sendOrReceive}`; const channel = CHANNELS.get(id); if (channel) { CHANNELS.delete(id); channel.close(); } }