UNPKG

tg-use-local-storage-state

Version:
165 lines (164 loc) 7.15 kB
import storage from './storage'; import { unstable_batchedUpdates } from 'react-dom'; import { useCallback, useEffect, useMemo, useReducer, useRef, } from '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); }, }); export default 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); } function useClientLocalStorageState(key, options) { const isFirstRender = useRef(true); const defaultValue = useRef(options === null || options === void 0 ? void 0 : options.defaultValue).current; // `id` changes every time a change in the `localStorage` occurs const [id, forceUpdate] = useReducer((number) => number + 1, 0); const setState = useCallback((newValue) => { const isCallable = (value) => typeof value === 'function'; const newUnwrappedValue = isCallable(newValue) ? newValue(storage.get(key, defaultValue, options)) : newValue; storage.set(key, newUnwrappedValue, options); 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 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 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.data.has(key) && localStorage.getItem(key) === null) { storage.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 = useRef(options === null || options === void 0 ? void 0 : options.ssr).current === true && isFirstRender.current; if (isFirstSsrRender && (storage.data.has(key) || defaultValue !== storage.get(key, defaultValue, options))) { forceUpdate(); isFirstRender.current = false; } return useMemo(() => [ isFirstSsrRender ? defaultValue : storage.get(key, defaultValue, options), setState, { isPersistent: isFirstSsrRender || !storage.data.has(key), removeItem() { storage.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; }