UNPKG

use-storage-state

Version:

React hook that you can wire with any Storage compatible API like `localStorage`, `sessionStorage`, or a custom one.

133 lines (132 loc) 5.38 kB
import memoryStorage from "./memoryStorage.js"; import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react"; export default function useStorageState(key, options) { const serializer = options?.serializer; const [defaultValue] = useState(options?.defaultValue); const storageOption = options !== undefined && "storage" in options ? options.storage : "local"; const storageObj = storageOption === "local" ? goodTry(() => localStorage) : storageOption === "session" ? goodTry(() => sessionStorage) : storageOption; const resolvedStorage = storageObj === undefined && options?.memoryFallback !== false ? memoryStorage : storageObj; return useStorage(key, defaultValue, resolvedStorage, options?.sync, options?.storeDefault, serializer?.parse, serializer?.stringify); } function useStorage(key, defaultValue, storage, sync = true, storeDefault = false, parse = parseJSON, stringify = JSON.stringify) { // we keep the `parsed` value in a ref because `useSyncExternalStore` requires a cached version const storageItem = useRef({ string: null, parsed: defaultValue, }); const value = useSyncExternalStore( // useSyncExternalStore.subscribe useCallback((onStoreChange) => { const onChange = (localKey) => { if (key === localKey) { onStoreChange(); } }; callbacks.add(onChange); return () => { callbacks.delete(onChange); }; }, [key]), // useSyncExternalStore.getSnapshot () => { const string = storage === undefined ? null : goodTry(() => storage.getItem(key)) ?? null; if (string !== storageItem.current.string) { let parsed; try { parsed = string === null ? defaultValue : parse(string); } catch { parsed = defaultValue; } storageItem.current.parsed = parsed; } storageItem.current.string = string; // related issues: // - https://github.com/astoilkov/use-local-storage-state/issues/26 // - https://github.com/astoilkov/use-storage-state/issues/1 if (storeDefault && string === null && storage !== undefined && defaultValue !== undefined) { // reasons for `localStorage` to throw an error: // - maximum quota is exceeded // - under Mobile Safari (since iOS 5) when the user enters private mode // `localStorage.setItem()` will throw // - trying to access localStorage object when cookies are disabled in Safari throws // "SecurityError: The operation is insecure." // eslint-disable-next-line no-console goodTry(() => { const string = stringify(defaultValue); storage.setItem(key, string); storageItem.current = { string, parsed: defaultValue }; }); } return storageItem.current.parsed; }, // useSyncExternalStore.getServerSnapshot () => defaultValue); const setState = useCallback((newValue) => { const value = newValue instanceof Function ? newValue(storageItem.current.parsed) : newValue; // reasons for `localStorage` to throw an error: // - maximum quota is exceeded // - under Mobile Safari (since iOS 5) when the user enters private mode // `localStorage.setItem()` will throw // - trying to access `localStorage` object when cookies are disabled in Safari throws // "SecurityError: The operation is insecure." goodTry(() => storage?.setItem(key, stringify(value))); triggerCallbacks(key); }, [key, storage, stringify]); const removeItem = useCallback(() => { if (storage !== undefined) { goodTry(() => storage.removeItem(key)); triggerCallbacks(key); } }, [key, storage]); // - syncs change across tabs, windows, iframes // - the `storage` event is called only in all tabs, windows, iframe's except the one that // triggered the change useEffect(() => { if (!sync) { return undefined; } const onStorage = (e) => { if (e.key === key && e.storageArea === goodTry(() => storage)) { triggerCallbacks(key); } }; window.addEventListener("storage", onStorage); return () => window.removeEventListener("storage", onStorage); }, [key, storage, sync]); return useMemo(() => [value, setState, removeItem], [value, setState, removeItem]); } // notifies all instances using the same `key` to update const callbacks = new Set(); function triggerCallbacks(key) { for (const callback of [...callbacks]) { callback(key); } } // a wrapper for `JSON.parse()` that supports "undefined" value. otherwise, // `JSON.parse(JSON.stringify(undefined))` returns the string "undefined" not the value `undefined` function parseJSON(value) { return value === "undefined" ? undefined : JSON.parse(value); } function goodTry(tryFn) { try { return tryFn(); } catch { } }