@react-hookz/web
Version:
React hooks done right, for browser and SSR.
203 lines (202 loc) • 8.28 kB
JavaScript
;
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;
}
};