UNPKG

@react-hookz/web

Version:

React hooks done right, for browser and SSR.

199 lines (198 loc) 8.06 kB
/* eslint-disable @typescript-eslint/no-use-before-define,no-use-before-define */ import { useCallback } from 'react'; import { useConditionalEffect, useFirstMountState, useIsomorphicLayoutEffect, useMountEffect, usePrevious, useSafeState, useSyncedRef, useUpdateEffect, } from '..'; import { resolveHookState } from "../util/resolveHookState.js"; import { isBrowser } from "../util/const.js"; import { off, on } from "../util/misc.js"; /** * Manages a single storage key. * * @param storage Storage instance that will be managed * @param key Storage key to manage * @param defaultValue Default value to yield in case the key is not in storage * @param options */ export function useStorageValue(storage, key, defaultValue, options) { if (defaultValue === void 0) { defaultValue = null; } if (options === void 0) { options = {}; } var isolated = options.isolated; var _a = options.initializeWithStorageValue, initializeWithStorageValue = _a === void 0 ? true : _a, _b = options.handleStorageEvent, handleStorageEvent = _b === void 0 ? true : _b, _c = options.storeDefaultValue, storeDefaultValue = _c === void 0 ? false : _c; // avoid fetching data from storage during SSR if (!isBrowser) { storeDefaultValue = false; initializeWithStorageValue = false; handleStorageEvent = false; } // needed to provide stable API var methods = useSyncedRef({ fetchVal: function () { return parse(storage.getItem(key), defaultValue); }, storeVal: function (val) { var stringified = stringify(val); if (stringified) { storage.setItem(key, stringified); return true; } return false; }, removeVal: function () { storage.removeItem(key); }, setVal: function (val) { setState(parse(val, defaultValue)); }, fetchState: function () { var newVal = methods.current.fetchVal(); setState(newVal); return newVal !== stateRef.current ? newVal : null; }, setState: function (nextState) { setState(nextState === null ? defaultValue : nextState); }, }); var isFirstMount = useFirstMountState(); var _d = useSafeState(initializeWithStorageValue && isFirstMount ? methods.current.fetchVal() : undefined), state = _d[0], setState = _d[1]; var prevState = usePrevious(state); var stateRef = useSyncedRef(state); var keyRef = useSyncedRef(key); var isolatedRef = useSyncedRef(isolated); // fetch value on mount for the case `initializeWithStorageValue` is false, // effects are not invoked during SSR, so there is no need to check isBrowser here useMountEffect(function () { if (!initializeWithStorageValue) { methods.current.fetchState(); } }); // store default value if it is not null and options configured to store default value useConditionalEffect(function () { methods.current.storeVal(defaultValue); }, undefined, [prevState !== state, storeDefaultValue && state === defaultValue && defaultValue !== null]); // refetch value when key changed useUpdateEffect(function () { methods.current.fetchState(); }, [key]); // subscribe hook for storage events useIsomorphicLayoutEffect(function () { if (!handleStorageEvent) return; // eslint-disable-next-line unicorn/consistent-function-scoping var storageHandler = function (ev) { if (ev.storageArea !== storage) return; if (ev.key !== keyRef.current) return; methods.current.setVal(ev.newValue); }; on(window, 'storage', storageHandler, { passive: true }); return function () { off(window, 'storage', storageHandler); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [handleStorageEvent]); // register hook for same-page synchronisation useIsomorphicLayoutEffect(function () { if (isolated) return; var storageKeys = storageKeysUsed.get(storage); if (!storageKeys) { storageKeys = new Map(); storageKeysUsed.set(storage, storageKeys); } var keySetters = storageKeys.get(key); if (!keySetters) { keySetters = new Set(); storageKeys.set(key, keySetters); } var mSetState = methods.current.setState; keySetters.add(mSetState); return function () { keySetters === null || keySetters === void 0 ? void 0 : keySetters.delete(mSetState); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isolated, key]); return [ state, useCallback(function (newState) { var _a, _b; if (!isBrowser) return; var s = resolveHookState(newState, stateRef.current); if (methods.current.storeVal(s)) { methods.current.setState(s); if (!isolatedRef.current) { // update all other hooks state (_b = (_a = storageKeysUsed .get(storage)) === null || _a === void 0 ? void 0 : _a.get(keyRef.current)) === null || _b === void 0 ? void 0 : _b.forEach(function (setter) { if (setter === methods.current.setState) return; setter(s); }); } } }, // eslint-disable-next-line react-hooks/exhaustive-deps []), useCallback(function () { var _a, _b; if (!isBrowser) return; methods.current.removeVal(); methods.current.setState(null); if (!isolatedRef.current) { // update all other hooks state (_b = (_a = storageKeysUsed .get(storage)) === null || _a === void 0 ? void 0 : _a.get(keyRef.current)) === null || _b === void 0 ? void 0 : _b.forEach(function (setter) { if (setter === methods.current.setState) return; setter(null); }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []), useCallback(function () { var _a, _b; if (!isBrowser) return; var newVal = methods.current.fetchState(); if (newVal !== null && !isolatedRef.current) { // update all other hooks state (_b = (_a = storageKeysUsed .get(storage)) === null || _a === void 0 ? void 0 : _a.get(keyRef.current)) === null || _b === void 0 ? void 0 : _b.forEach(function (setter) { if (setter === methods.current.setState) return; setter(newVal); }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []), ]; } var storageKeysUsed = new Map(); var stringify = function (data) { if (data === null) { // eslint-disable-next-line no-console 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) /* istanbul ignore next */ { // i have absolutely no idea how to cover this, since modern JSON.stringify does not throw on // cyclic references anymore // eslint-disable-next-line no-console console.warn(error); return null; } }; var parse = function (str, fallback) { if (str === null) return fallback; try { return JSON.parse(str); } catch (error) { // eslint-disable-next-line no-console console.warn(error); return fallback; } };