import {useEffect, useMemo, useRef, useState} from "react"; const getStorageMap = (storageGetter) => { let storage; try { storage = storageGetter(); } catch { return { storageGet() { return null; }, storageSet() { return false; }, storageRemove() { return false; }, }; } return { storageGet(key) { try { const content = storage.getItem(key); return content ? JSON.parse(content) : null; } catch (e) { return null; } }, storageSet(key, val) { try { storage.setItem(key, JSON.stringify(val)); return true; } catch (e) { return false; } }, storageRemove(key) { try { storage.removeItem(key); return true; } catch (e) { return false; } }, }; }; /** * `lsStorage` contains a wrapper for the provided storage (either `localStorage` or `sessionStorage`) * that automatically transform data from and to json. * It also deals nicely with browsers that forbid accessing `localStorage` */ const storageWrapper = getStorageMap(() => window.localStorage); const getStorageValOrDefaultWithKey = (key, defaultVal) => { if (!key) return {key, value: null}; const storageVal = storageWrapper.storageGet(key); const value = storageVal === null ? defaultVal : storageVal; return {key, value}; }; const listeners = new Set(); const addListener = (cb) => { listeners.add(cb); return () => listeners.delete(cb); }; const notifyListeners = (...args) => { listeners.forEach((l) => l(...args)); }; /** * sample usage: * const [val, setVal] = useLocalStorage('my-storage-key') * * full example: * const [val, setVal, {clear}] = useLocalStorage('my-storage-key', [defaultValueIfStorageHasNoValueYet]) * */ export const useLocalStorageState = (key, defaultVal) => { // `data` contains both the current key and the value. The `key` is stored to allow // reacting immediately in case the key passed in above has changed. (rather than having to wait for an e.g. `useEffect`) const [data, setData] = useState(() => getStorageValOrDefaultWithKey(key, defaultVal)); // `meRef` contains a unique identifier. This way the event listener knows which hook has been calling // and doesn't need to be updated again const meRef = useRef(); if (!meRef.current) meRef.current = {}; // nextVal is a fallback only used when the key changes so we can immediately pass the `key`'s real value let nextVal = null; // if the passed key differs from the last seen key, update the data immediately if (key !== data.key) { nextVal = getStorageValOrDefaultWithKey(key, defaultVal); setData(nextVal); } // start listening for changes in case another hook changes the same key useEffect( () => addListener((eventKey, val, by) => { if (key !== eventKey) return; if (by === meRef.current) return; setData(val); }), [key] ); // create a reference of the passed `defaultVal`. This allows the `useMemo` below to access the defaultVal // without having to add it to the dependency array. (Otherwise calling e.g. `useLocalStorageState('key', [])`) // would result in not really memoizing the handlers below as the `[]` is always a new array different from the // one passed in before. const defaultValueRef = useRef(defaultVal); useEffect(() => { defaultValueRef.current = defaultVal; }, [defaultVal]); // memoize `setVal` and `clear` as setters are expected to not change in hooks. const handlers = useMemo( () => ({ // setVal supports both `setVal(nextValue)` and `setVal(prevVal => prevVal + 1)` shapes setVal: (next) => { if (typeof next === "function") { setData((prev) => { const val = next(prev.value); storageWrapper.storageSet(key, val); notifyListeners(key, val, meRef.current); return {key, value: val}; }); } else { storageWrapper.storageSet(key, next); setData({key, value: next}); notifyListeners(key, next, meRef.current); } }, clear: () => { storageWrapper.storageRemove(key); setData({key, value: defaultValueRef.current}); notifyListeners(key, defaultValueRef.current, meRef.current); }, }), [key] ); return [nextVal ? nextVal.value : data.value, handlers.setVal, {clear: handlers.clear}]; };