import { Dispatch, SetStateAction, useCallback, useLayoutEffect, useMemo, useRef, useState, } from 'react'; import { canUseDOM, noop } from './utils'; export type UseLocalStorageOptions = | { raw: true; } | { deserializer: (value: string) => TValue; raw: false; serializer: (value: TValue) => string; }; export type UseLocalStorageResult = [ value: TValue | undefined, setValue: Dispatch>, remove: () => void, ]; function useLocalStorageHook( key: string, initialValue?: TValue, options?: UseLocalStorageOptions, ): UseLocalStorageResult { if (!key) { throw new Error('useLocalStorage: "key" is required'); } const deserializer = useMemo( () => (options?.raw ? (value: any) => value : (options?.deserializer ?? JSON.parse)), [options], ); const serializer = useMemo( () => (options?.raw ? String : (options?.serializer ?? JSON.stringify)), [options], ); const initializer = useRef((k: string) => { try { const localStorageValue = localStorage.getItem(k); if (localStorageValue !== null) { return deserializer(localStorageValue); } initialValue && localStorage.setItem(k, serializer(initialValue)); return initialValue; /* c8 ignore next 6 */ } catch { // If user is in private mode or has storage restriction // localStorage can throw. JSON.parse and JSON.stringify // can throw, too. return initialValue; } }); const [state, setState] = useState(() => initializer.current(key)); useLayoutEffect(() => setState(initializer.current(key)), [key]); const set: Dispatch> = useCallback( patch => { try { const newState = patch instanceof Function ? patch(state) : patch; if (typeof newState === 'undefined') { return; } let value: string; if (options) { if (options.raw) { value = typeof newState === 'string' ? newState : JSON.stringify(newState); } else if (options?.serializer) { value = options.serializer(newState); } else { value = JSON.stringify(newState); } } else { value = JSON.stringify(newState); } localStorage.setItem(key, value); setState(deserializer(value)); /* c8 ignore next 4 */ } catch { // If user is in private mode or has storage restriction // localStorage can throw. Also, JSON.stringify can throw. } }, [deserializer, key, options, state], ); const remove = useCallback(() => { try { localStorage.removeItem(key); setState(undefined); /* c8 ignore next 4 */ } catch { // If user is in private mode or has storage restriction // localStorage can throw. } }, [key, setState]); return [state, set, remove]; } function useLocalStorageSSR( _key: string, initialValue?: TValue, _options?: UseLocalStorageOptions, ): UseLocalStorageResult { return [initialValue, noop, noop]; } export const useLocalStorage = canUseDOM() ? useLocalStorageHook : useLocalStorageSSR;