tg-use-local-storage-state
Version:
React hook that persists data in localStorage
171 lines (170 loc) • 7.57 kB
JavaScript
;
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;
}