@kadconsulting/dry
Version:
KAD Reusable Component Library
181 lines • 7.48 kB
JavaScript
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