From 4f7dd14c9fafff49757fbee796b193a5c6fea64d Mon Sep 17 00:00:00 2001 From: DISCONNECTED-png Date: Tue, 5 May 2026 23:07:37 +0530 Subject: [PATCH] fix(useLocalStorage): return null after explicit removal instead of initialValue When setState(null) is called, item is removed from storage but the hook was returning initialValue instead of null. Two root causes: 1. Return line fell back to initialValue when store was null, with no way to distinguish 'never set' from 'intentionally cleared' 2. useEffect re-seeded initialValue back into storage immediately after removal Fix: track intentional removals with wasRemovedRef so both the effect and the return value handle the cleared state correctly. Fixes: #344 --- index.js | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) 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() {