UNPKG

@react-hookz/web

Version:

React hooks done right, for browser and SSR.

203 lines (202 loc) 8.28 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useStorageValue = void 0; /* eslint-disable @typescript-eslint/no-use-before-define,no-use-before-define */ var react_1 = require("react"); var __1 = require(".."); var resolveHookState_1 = require("../util/resolveHookState"); var const_1 = require("../util/const"); var misc_1 = require("../util/misc"); /** * 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 */ 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 (!const_1.isBrowser) { storeDefaultValue = false; initializeWithStorageValue = false; handleStorageEvent = false; } // needed to provide stable API var methods = (0, __1.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 = (0, __1.useFirstMountState)(); var _d = (0, __1.useSafeState)(initializeWithStorageValue && isFirstMount ? methods.current.fetchVal() : undefined), state = _d[0], setState = _d[1]; var prevState = (0, __1.usePrevious)(state); var stateRef = (0, __1.useSyncedRef)(state); var keyRef = (0, __1.useSyncedRef)(key); var isolatedRef = (0, __1.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 (0, __1.useMountEffect)(function () { if (!initializeWithStorageValue) { methods.current.fetchState(); } }); // store default value if it is not null and options configured to store default value (0, __1.useConditionalEffect)(function () { methods.current.storeVal(defaultValue); }, undefined, [prevState !== state, storeDefaultValue && state === defaultValue && defaultValue !== null]); // refetch value when key changed (0, __1.useUpdateEffect)(function () { methods.current.fetchState(); }, [key]); // subscribe hook for storage events (0, __1.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); }; (0, misc_1.on)(window, 'storage', storageHandler, { passive: true }); return function () { (0, misc_1.off)(window, 'storage', storageHandler); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [handleStorageEvent]); // register hook for same-page synchronisation (0, __1.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, (0, react_1.useCallback)(function (newState) { var _a, _b; if (!const_1.isBrowser) return; var s = (0, resolveHookState_1.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 []), (0, react_1.useCallback)(function () { var _a, _b; if (!const_1.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 }, []), (0, react_1.useCallback)(function () { var _a, _b; if (!const_1.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 }, []), ]; } exports.useStorageValue = useStorageValue; 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; } };