UNPKG

tg-use-local-storage-state

Version:
171 lines (170 loc) 7.57 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const storage_1 = __importDefault(require("./storage")); const react_dom_1 = require("react-dom"); const react_1 = require("react"); // `activeHooks` holds all active hooks. we use the array to update all hooks with the same key — // calling `setValue` of one hook triggers an update for all other hooks with the same key const activeHooks = []; // interface SameTabStorageEvent extends CustomEvent { // /** Returns the key of the storage item being changed. */ // readonly detail: {key: string}; // } Storage.prototype.setItem = new Proxy(Storage.prototype.setItem, { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type apply(target, thisArg, argumentList) { const event = new CustomEvent("sameTabStorage", { detail: { key: argumentList[0], oldValue: thisArg.getItem(argumentList[0]), newValue: argumentList[1], }, }); window.dispatchEvent(event); return Reflect.apply(target, thisArg, argumentList); }, }); Storage.prototype.removeItem = new Proxy(Storage.prototype.removeItem, { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type apply(target, thisArg, argumentList) { const event = new CustomEvent("sameTabStorage", { detail: { key: argumentList[0], }, }); window.dispatchEvent(event); return Reflect.apply(target, thisArg, argumentList); }, }); Storage.prototype.clear = new Proxy(Storage.prototype.clear, { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type apply(target, thisArg, argumentList) { const event = new CustomEvent("sameTabStorage", { detail: { key: "__all__", }, }); window.dispatchEvent(event); return Reflect.apply(target, thisArg, argumentList); }, }); function useLocalStorageState(key, options) { // SSR support if (typeof window === 'undefined') { return [ options === null || options === void 0 ? void 0 : options.defaultValue, () => { }, { isPersistent: true, removeItem: () => { } }, ]; } // eslint-disable-next-line react-hooks/rules-of-hooks return useClientLocalStorageState(key, options); } exports.default = useLocalStorageState; function useClientLocalStorageState(key, options) { const isFirstRender = (0, react_1.useRef)(true); const defaultValue = (0, react_1.useRef)(options === null || options === void 0 ? void 0 : options.defaultValue).current; // `id` changes every time a change in the `localStorage` occurs const [id, forceUpdate] = (0, react_1.useReducer)((number) => number + 1, 0); const setState = (0, react_1.useCallback)((newValue) => { const isCallable = (value) => typeof value === 'function'; const newUnwrappedValue = isCallable(newValue) ? newValue(storage_1.default.get(key, defaultValue, options)) : newValue; storage_1.default.set(key, newUnwrappedValue, options); (0, react_dom_1.unstable_batchedUpdates)(() => { for (const update of activeHooks) { if (update.key === key) { update.forceUpdate(); } } }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [key, defaultValue]); // - syncs change across tabs, windows, iframe's // - the `storage` event is called only in all tabs, windows, iframe's except the one that // triggered the change (0, react_1.useEffect)(() => { const onStorage = (e) => { if (e.storageArea === localStorage && e.key === key) { forceUpdate(); } }; const onStorageSameTab = (e) => { var _a; if (!isCustomEvent(e)) { throw new Error('not a custom event'); } if (((_a = e.detail) === null || _a === void 0 ? void 0 : _a.key) === key || key === "__all__") { forceUpdate(); } }; window.addEventListener('storage', onStorage); window.addEventListener("sameTabStorage", onStorageSameTab); return () => { window.removeEventListener("sameTabStorage", onStorageSameTab); window.removeEventListener('storage', onStorage); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [key]); // add this hook to the `activeHooks` array. see the `activeHooks` declaration above for a // more detailed explanation (0, react_1.useEffect)(() => { const entry = { key, forceUpdate }; activeHooks.push(entry); return () => { activeHooks.splice(activeHooks.indexOf(entry), 1); }; }, [key]); // initial issue: https://github.com/astoilkov/use-local-storage-state/issues/26 // issues that were caused by incorrect initial and secondary implementations: // - https://github.com/astoilkov/use-local-storage-state/issues/30 // - https://github.com/astoilkov/use-local-storage-state/issues/33 if (defaultValue !== undefined && !storage_1.default.data.has(key) && localStorage.getItem(key) === null) { storage_1.default.set(key, defaultValue, options); } // - SSR support // - not inside a `useLayoutEffect` because this way we skip the calls to `useEffect()` and // `useLayoutEffect()` for the first render (which also increases performance) // - inspired by: https://github.com/astoilkov/use-local-storage-state/pull/40 // - related: https://github.com/astoilkov/use-local-storage-state/issues/39 // - related: https://github.com/astoilkov/use-local-storage-state/issues/43 const isFirstSsrRender = (0, react_1.useRef)(options === null || options === void 0 ? void 0 : options.ssr).current === true && isFirstRender.current; if (isFirstSsrRender && (storage_1.default.data.has(key) || defaultValue !== storage_1.default.get(key, defaultValue, options))) { forceUpdate(); isFirstRender.current = false; } return (0, react_1.useMemo)(() => [ isFirstSsrRender ? defaultValue : storage_1.default.get(key, defaultValue, options), setState, { isPersistent: isFirstSsrRender || !storage_1.default.data.has(key), removeItem() { storage_1.default.remove(key); for (const update of activeHooks) { if (update.key === key) { update.forceUpdate(); } } }, }, ], // disabling eslint warning for the following reasons: // - `id` is needed because when it changes that means the data in `localStorage` has // changed and we need to update the returned value. However, the eslint rule wants us to // remove the `id` from the dependencies array. // - `defaultValue` never changes so we can skip it and reduce package size // - `setState` changes when `key` changes so we can skip it and reduce package size // eslint-disable-next-line react-hooks/exhaustive-deps [id, key]); } function isCustomEvent(event) { return 'detail' in event; }