UNPKG

@supunlakmal/hooks

Version:

A collection of reusable React hooks

201 lines 6.9 kB
import { useEffect, useMemo, useState } from 'react'; import { useFirstMountState } from '../state-management/useFirstMountState'; import { useIsomorphicLayoutEffect } from '../side-effects-lifecycle/useIsomorphicLayoutEffect'; import { useSyncedRef } from '../performance-optimization/useSyncedRef'; import { useUpdateEffect } from '../side-effects-lifecycle/useUpdateEffect'; import { isBrowser } from '../../util/const'; import { off, on } from '../../util/misc'; import { resolveHookState, } from '../../util/resolve-hook-state'; const storageListeners = new Map(); const invokeStorageKeyListeners = (s, key, value, skipListener) => { var _a; const listeners = (_a = storageListeners.get(s)) === null || _a === void 0 ? void 0 : _a.get(key); if (listeners === undefined || listeners.size === 0) { return; } for (const listener of listeners) { if (listener !== skipListener) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call listener(value); } } }; const storageEventHandler = (evt) => { if (evt.storageArea && evt.key && evt.newValue) { invokeStorageKeyListeners(evt.storageArea, evt.key, evt.newValue); } }; const addStorageListener = (s, key, listener) => { // In case of first listener added within browser environment we // want to bind single storage event handler if (isBrowser && storageListeners.size === 0) { on(globalThis, 'storage', storageEventHandler, { passive: true }); } let keys = storageListeners.get(s); if (!keys) { keys = new Map(); storageListeners.set(s, keys); } let listeners = keys.get(key); if (!listeners) { listeners = new Set(); keys.set(key, listeners); } listeners.add(listener); }; const removeStorageListener = (s, key, listener) => { const keys = storageListeners.get(s); if (!keys) { return; } const listeners = keys.get(key); if (!listeners) { return; } listeners.delete(listener); if (listeners.size === 0) { keys.delete(key); } if (keys.size === 0) { storageListeners.delete(s); } // Unbind storage event handler in browser environment in case there is no // storage keys listeners left if (isBrowser && storageListeners.size === 0) { off(globalThis, 'storage', storageEventHandler); } }; const DEFAULT_OPTIONS = { defaultValue: null, initializeWithValue: true, }; export const useStorageValue = (storage, key, options) => { var _a; const optionsRef = useSyncedRef(Object.assign(Object.assign({}, DEFAULT_OPTIONS), options)); const parse = (str, fallback) => { var _a; const parseFunction = (_a = optionsRef.current.parse) !== null && _a !== void 0 ? _a : defaultParse; return parseFunction(str, fallback); }; const stringify = (data) => { var _a; const stringifyFunction = (_a = optionsRef.current.stringify) !== null && _a !== void 0 ? _a : defaultStringify; return stringifyFunction(data); }; const storageActions = useSyncedRef({ fetchRaw: () => storage.getItem(key), fetch: () => parse(storageActions.current.fetchRaw(), optionsRef.current.defaultValue), remove() { storage.removeItem(key); }, store(value) { const stringified = stringify(value); if (stringified !== null) { storage.setItem(key, stringified); } return stringified; }, }); const isFirstMount = useFirstMountState(); const [state, setState] = useState(((_a = optionsRef.current) === null || _a === void 0 ? void 0 : _a.initializeWithValue) && isFirstMount ? storageActions.current.fetch() : undefined); const stateRef = useSyncedRef(state); const stateActions = useSyncedRef({ fetch() { setState(storageActions.current.fetch()); }, setRawVal(value) { setState(parse(value, optionsRef.current.defaultValue)); }, }); useUpdateEffect(() => { stateActions.current.fetch(); }, [key]); useEffect(() => { if (!optionsRef.current.initializeWithValue) { stateActions.current.fetch(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useIsomorphicLayoutEffect(() => { const handler = stateActions.current.setRawVal; addStorageListener(storage, key, handler); return () => { removeStorageListener(storage, key, handler); }; }, [storage, key]); const actions = useSyncedRef({ set(value) { if (!isBrowser) { return; } const s = resolveHookState(value, stateRef.current); const storeValue = storageActions.current.store(s); if (storeValue !== null) { invokeStorageKeyListeners(storage, key, storeValue); } }, delete() { if (!isBrowser) { return; } storageActions.current.remove(); invokeStorageKeyListeners(storage, key, null); }, fetch() { if (!isBrowser) { return; } invokeStorageKeyListeners(storage, key, storageActions.current.fetchRaw()); }, }); // Make actions static so developers can pass methods further const staticActions = useMemo(() => ({ set: ((v) => { actions.current.set(v); }), remove() { actions.current.delete(); }, fetch() { actions.current.fetch(); }, }), // eslint-disable-next-line react-hooks/exhaustive-deps []); return useMemo(() => (Object.assign({ value: state }, staticActions)), // eslint-disable-next-line react-hooks/exhaustive-deps [state]); }; const defaultStringify = (data) => { if (data === null) { if (process.env.NODE_ENV === 'development') { console.warn("'null' is not a valid data for useStorageValue hook, this operation will take no effect"); } return null; } try { return JSON.stringify(data); } catch (error) /* v8 ignore next */ { // I have absolutely no idea how to cover this, since modern JSON.stringify does not throw on // cyclic references anymore console.warn(error); return null; } }; const defaultParse = (str, fallback) => { if (str === null) { return fallback; } try { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return JSON.parse(str); } catch (error) { console.warn(error); return fallback; } }; //# sourceMappingURL=useStorageValue.js.map