diff --git a/index.js b/index.js index f6e4fe2..8e49f87 100644 --- a/index.js +++ b/index.js @@ -622,14 +622,29 @@ export function useLocalStorage(key, initialValue) { getLocalStorageServerSnapshot ); + // Tracks whether the item was explicitly removed via setState(null/undefined). + // This lets us distinguish "never initialized" from "intentionally cleared", + // which the storage API itself has no concept of. + const wasRemovedRef = React.useRef(false); + + // Reset the removal flag when the key changes — switching keys means + // we're managing a different value entirely, so start fresh. + const prevKeyRef = React.useRef(key); + if (prevKeyRef.current !== key) { + prevKeyRef.current = key; + wasRemovedRef.current = false; + } + const setState = React.useCallback( (v) => { try { const nextState = typeof v === "function" ? v(JSON.parse(store)) : v; if (nextState === undefined || nextState === null) { + wasRemovedRef.current = true; removeLocalStorageItem(key); } else { + wasRemovedRef.current = false; setLocalStorageItem(key, nextState); } } catch (e) { @@ -640,15 +655,27 @@ export function useLocalStorage(key, initialValue) { ); React.useEffect(() => { + // Only seed the initial value if the key is absent AND the user hasn't + // explicitly cleared it. Without this guard, calling setState(null) would + // remove the item, but this effect would immediately write initialValue back. if ( getLocalStorageItem(key) === null && - typeof initialValue !== "undefined" + typeof initialValue !== "undefined" && + !wasRemovedRef.current ) { setLocalStorageItem(key, initialValue); } }, [key, initialValue]); - return [store ? JSON.parse(store) : initialValue, setState]; + // If the item is absent because it was explicitly removed, return null. + // If it was never set, fall back to initialValue as before. + const resolvedValue = store + ? JSON.parse(store) + : wasRemovedRef.current + ? null + : initialValue; + + return [resolvedValue, setState]; } export function useLockBodyScroll() {