UNPKG

@kadconsulting/dry

Version:
181 lines 7.48 kB
import { LocalStorageValueTypes, } from './types'; import { useCallback, useState } from 'react'; /** Expose these values in the module so that tests can mock localStorage implementation */ export const FEATURE_DETECTION_TEST_KEY = 'test-key'; export const FEATURE_DETECTION_TEST_VALUE = 'test-value'; /** * @name useLocalStorageForKey * @description * Abstracts away cumbersome serialization/deserialization, error handling, * feature detection, SSR and type casting when using localStorage. Can also * condense call signatures slightly by constraining usage to one key at a time. * * @example * Here, we're letting the hook know that it's parsing an object type. We use * the shouldThrow option when calling set in order to log some hypothetical * warning about a user's degraded experience for analytics purposes if the set * operation fails. * ``` * // Consume the hook * const currentUser = useLocalStorageForKey( * LocalStorageKeys.CURRENT_USER, * { valueType: LocalStorageValueTypes.OBJECT } * ); * * // ...later, in some callback * * try { * if (currentUser.get()) return; // User already cached, no need to fetch * // Use HTTP-only cookie to fetch current user * const response = await fetch('/api/current-user'); * const currentUser = await response.json(); * // Cache the current user so that subsequent visits don't require a round-trip to the server * const storageCurrentUser.set(currentUser, { shouldThrow: true }); * } catch (error) { * await logger.warn(`Unable to set currentUser in localStorage: ${error.message}. User ${currentUser.id} will face increased round-trip latency on subsequent visits due to lack of cache.`) * }} * ``` * @example * Here, we've used localStorage to cache whether the user responded to a * legal drinking age prompt. We use the debug option to log errors to the console. * We use the value returned to prevent the modal from showing on subsequent visits. * We swallow any errors by not opting into error handling via the `shouldThrow` * option, because it's not the end of the world if the user has to see the modal * again on subsequent visits. * ``` * // Consume the hook, use the result to set initial state * const didAcknowlegeAgeRestriction = useLocalStorageForKey( * LocalStorageKeys.DID_ACKNOWLEDGE_AGE_RESTRICTION, * { debug: true, valueType: LocalStorageValueTypes.BOOLEAN } * ) * * const [ * shouldShowLegalDrinkingAgeModal, * setShouldShowLegalDrinkingAgeModal * ] = useState(didAcknowlegeAgeRestrictionStorage.get() || false) * ``` */ export const useLocalStorageForKey = (key, { /** * If true, logs an error message to the console in the event an error is caught. * Errors can be serialization/deserialization errors if unserializable values are * passed to be stringified / parsed, or they can be errors thrown by the browser * if localStorage has been disabled (private browsing mode, storage quota exceeded, * or the user has prevented the browser from persisting data). */ debug = false, valueType, }) => { /** * Check whether localStorage is isSupported on mount; if all checks pass, isSupported * state is updated and hook is usable. Uses modernizr's approach to localStorage * feature detection. See: https://stackoverflow.com/questions/16427636/check-if-localstorage-is-available */ const computeInitialValue = useCallback(() => { if (typeof window === 'undefined') return false; // SSR try { localStorage.setItem(FEATURE_DETECTION_TEST_KEY, FEATURE_DETECTION_TEST_VALUE); if (localStorage.getItem(FEATURE_DETECTION_TEST_KEY) === FEATURE_DETECTION_TEST_VALUE) { localStorage.removeItem(FEATURE_DETECTION_TEST_KEY); return true; } return false; } catch (error) { if (debug) console.error(error); return false; } }, [debug]); const [ /** Whether the current execution context supports Storage */ isSupported,] = useState(computeInitialValue()); /** @throws */ const checkSupported = useCallback(() => { if (!isSupported) throw new Error('localStorage not supported in current context'); }, [isSupported]); /** @throws */ const deserialize = useCallback((value) => { /** It's not safe to JSON.parse strings as they'll likely be invalid JSON */ if (valueType === LocalStorageValueTypes.STRING) return value; try { const deserialized = JSON.parse(value); /** If JSON.parse succeeds, the value could (unlikely) still be an unexpected type */ // rome-ignore lint/suspicious/useValidTypeof: Here, we're using an enum, whose associated values ARE valid typeof string values if (typeof deserialized !== valueType) throw new Error(`Expected ${valueType} from storage, but received ${typeof deserialized} for value ${value} at key ${key}`); return deserialized; } catch (error) { if (debug) console.error(error); throw new Error(`Unable to deserialize value ${value} for key ${key} to type ${valueType}`); } }, [valueType, debug, key]); /** * @description * `localStorage.getItem`, but with built-in feature-detection, `JSON.parse` deserialization, and opt-in error handling * @throws */ const get = useCallback(() => { try { checkSupported(); const value = localStorage.getItem(key); const deserialized = value ? deserialize(value) : null; return deserialized; } catch (error) { if (debug) console.error(error); return null; } }, [debug, deserialize, checkSupported, key]); /** * @description * `localStorage.setItem`, but with built-in feature detection, `JSON.stringify` serialization, and opt-in error handling * @throws */ const set = useCallback((value, options = {}) => { try { checkSupported(); const serialized = // rome-ignore lint/suspicious/useValidTypeof: Here, we're using an enum, whose associated values ARE valid typeof string values typeof valueType === LocalStorageValueTypes.STRING ? value : JSON.stringify(value); localStorage.setItem(key, serialized); } catch (error) { if (debug) console.error(error); if (options.shouldThrow) throw new Error(`Unable to setItem ${value} for key ${key}`); } }, [checkSupported, key, valueType, debug]); /** * @description * `localStorage.removeItem`, but with built-in feature detection and opt-in error handling * @throws */ const remove = useCallback((options = {}) => { try { checkSupported(); localStorage.removeItem(key); } catch (error) { if (debug) console.error(error); if (options.shouldThrow) throw new Error(`Unable to removeItem for key ${key}`); } }, [debug, checkSupported, key]); return { get, set, remove, isSupported, }; }; //# sourceMappingURL=useLocalStorageForKey.js.map