UNPKG

react-storage-complete

Version:

🗄️ React hooks for accessing localStorage and sessionStorage, with syncing and prefix support. The complete package.

179 lines (178 loc) 9.55 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.storageEventEmitter = exports.EMITTER_CHANGE_EVENT_NAME = exports.defaultDecode = exports.defaultEncode = exports.useBrowserStorage = exports.DEFAULT_BROWSER_STORAGE_OPTIONS = void 0; const react_1 = __importDefault(require("react")); const events_1 = __importDefault(require("events")); const react_sub_unsub_1 = require("react-sub-unsub"); const useClientReady_1 = require("./useClientReady"); exports.DEFAULT_BROWSER_STORAGE_OPTIONS = { prefix: undefined, prefixSeparator: '.', shouldInitialize: true, emitterDisabled: false, storageEventListenerDisabled: false, encode: defaultEncode, decode: defaultDecode, }; function useBrowserStorage(key, defaultWhenUndefined, storage, options = exports.DEFAULT_BROWSER_STORAGE_OPTIONS) { const opts = react_1.default.useMemo(() => { return Object.assign(Object.assign({}, exports.DEFAULT_BROWSER_STORAGE_OPTIONS), options); }, [ options.prefix, options.prefixSeparator, options.shouldInitialize, options.emitterDisabled, options.storageEventListenerDisabled, ]); // Don't include `options` or the encoders, they may change on every render const clientReady = (0, useClientReady_1.useClientReady)(); const [initialized, setInitialized] = react_1.default.useState(!!opts.shouldInitialize); const hookUuid = react_1.default.useRef(uuid()); const scopedStorageKey = react_1.default.useMemo(() => `${opts.prefix ? `${opts.prefix}${opts.prefixSeparator}` : ''}${key}`, [key, opts.prefix, opts.prefixSeparator]); const defaultValue = react_1.default.useMemo(() => defaultWhenUndefined, []); // Don't include defaultWhenUndefined, it may change on every render const getDecodedValue = react_1.default.useCallback(() => { let val = defaultValue; if (opts.shouldInitialize && typeof storage !== 'undefined' && opts.decode) { try { const storedRawVal = storage[scopedStorageKey]; val = typeof storedRawVal === 'undefined' ? defaultValue : opts.decode(storedRawVal); } catch (e) { console.error('Error while decoding stored value for key:', scopedStorageKey, 'Error:', e, 'Value was:', storage[scopedStorageKey], 'This value could not be decoded properly. Try 1) checking the value, 2) checking your decoder, or 3) if using the default decoder (which uses JSON.parse), try specifying your own.'); } } return val; }, [defaultValue, opts, scopedStorageKey, storage]); const [state, setState] = react_1.default.useState(getDecodedValue()); // If the scope, key, storage, or opts.shouldInitialize change, decode and set the value, and set it as initialized. react_1.default.useEffect(() => { const currentEncodedState = state && opts.encode ? opts.encode(state) : undefined; if (opts.shouldInitialize && clientReady) { const newEncodedValue = storage[scopedStorageKey]; if (currentEncodedState !== newEncodedValue) { setState(getDecodedValue()); } setInitialized(true); } else { const newEncodedValue = defaultValue && opts.encode ? opts.encode(defaultValue) : undefined; if (currentEncodedState !== newEncodedValue) { setState(defaultValue); } // When there's no scope, set as not initialized setInitialized(false); } }, [getDecodedValue, opts.shouldInitialize, defaultValue, scopedStorageKey, clientReady]); // Sync with all hook instances through an emitter react_1.default.useEffect(() => { const subs = new react_sub_unsub_1.Subs(); if (!opts.emitterDisabled) { const changeEventListener = (changedKey, storageArea, sourceUuid) => { if (scopedStorageKey === changedKey && storage === storageArea && hookUuid.current !== sourceUuid) { try { setState(getDecodedValue()); } catch (e) { console.error(e); } } }; subs.subscribeEvent(exports.storageEventEmitter, exports.EMITTER_CHANGE_EVENT_NAME, changeEventListener); } return subs.createCleanup(); }, [getDecodedValue, opts.emitterDisabled, storage, scopedStorageKey]); // Sync with other open browser tabs via Window Storage Events // See: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event react_1.default.useEffect(() => { const subs = new react_sub_unsub_1.Subs(); if (!opts.storageEventListenerDisabled) { const storageEventListener = (e) => { if (e.storageArea === storage) { const changedKey = e.key; if (scopedStorageKey === changedKey) { try { setState(getDecodedValue()); } catch (e) { console.error(e); } } } }; subs.subscribeDOMEvent(window, 'storage', storageEventListener); } return subs.createCleanup(); }, [getDecodedValue, opts.storageEventListenerDisabled, scopedStorageKey, storage]); const setStateCombined = react_1.default.useCallback((value) => { if (opts.shouldInitialize && clientReady) { try { if (opts.encode && opts.decode && typeof value !== 'undefined') { const encodedState = state ? opts.encode(state) : undefined; const encodedValue = opts.encode(value); // Only set the state if the encoded value is different. if (encodedState !== encodedValue) { try { // Ensure we can decode it, too opts.decode(encodedValue); setState(value); storage[scopedStorageKey] = encodedValue; if (!opts.emitterDisabled) { exports.storageEventEmitter.emit(exports.EMITTER_CHANGE_EVENT_NAME, scopedStorageKey, storage, hookUuid.current); } } catch (e) { console.error('Error while testing decoding during set operation:', scopedStorageKey, 'Error:', e, 'Value was:', value, 'Could not decode after encoded as:', encodedValue, 'This value could not be decoded properly. Try 1) checking the value, 2) checking your decoder, or 3) if using the default decoder (which uses JSON.parse), try specifying your own.'); } } } if (typeof value === 'undefined' && typeof state !== 'undefined') { setState(defaultValue); delete storage[scopedStorageKey]; if (!opts.emitterDisabled) { exports.storageEventEmitter.emit(exports.EMITTER_CHANGE_EVENT_NAME, scopedStorageKey, storage, hookUuid.current); } } } catch (e) { console.error('Error while encoding:', scopedStorageKey, 'Error:', e, 'Bad value was:', value, 'This value could not be encoded properly. Try 1) checking the value as you may have provided a value that the encoder could not convert to a string, 2) checking your encoder, or 3) if using the default encoder (which uses JSON.stringify), try specifying your own.'); } } }, [opts, scopedStorageKey, state, storage, clientReady]); const clear = react_1.default.useCallback(() => { setStateCombined(undefined); }, [setStateCombined]); return [state, setStateCombined, initialized, clear, scopedStorageKey]; } exports.useBrowserStorage = useBrowserStorage; function defaultEncode(value) { return value !== null ? JSON.stringify(value) : null; } exports.defaultEncode = defaultEncode; function defaultDecode(itemString) { return itemString !== null ? JSON.parse(itemString) : null; } exports.defaultDecode = defaultDecode; exports.EMITTER_CHANGE_EVENT_NAME = 'change'; exports.storageEventEmitter = new events_1.default(); exports.storageEventEmitter.setMaxListeners(100); // Source: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid const uuid = () => { let d = new Date().getTime(); //Timestamp let d2 = (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0; //Time in microseconds since page-load or 0 if unsupported return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { let r = Math.random() * 16; //random number between 0 and 16 if (d > 0) { //Use timestamp until depleted r = (d + r) % 16 | 0; d = Math.floor(d / 16); } else { //Use microseconds since page-load if supported r = (d2 + r) % 16 | 0; d2 = Math.floor(d2 / 16); } return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); }); };