React useLocalStorage Hook无法更新LocalStorage值问题排查
问题:调用setUser后LocalStorage仅保存null值的原因分析
代码背景
自定义Hook useLocalStorage 和辅助函数
function getDefaultValue<T>(key: string, initialValue: T | null): T | null { const storedValue: string | null = localStorage.getItem(key); if (storedValue) { return JSON.parse(storedValue); } if (initialValue instanceof Function) { initialValue(); } return initialValue; } export function useLocalStorage<T>( initialValue: T | null, key: string ): [T | null, React.Dispatch<React.SetStateAction<T | null>>] { const [value, setValue] = useState<T | null>( getDefaultValue(key, initialValue) ); useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]); return [value, setValue]; }
封装的认证Hook useAuth
export function useAuth(): { user: User | null; setUser: React.Dispatch<React.SetStateAction<User | null>>; } { const [user, setUser] = useLocalStorage<User>(null, "user-data"); return { user, setUser, }; }
登录逻辑代码
const { setUser } = useAuth(); const handleLogin = () => { console.log(emailRef.current?.value); console.log(passwordRef.current?.value); console.log(thirtyDaysCheckboxRef.current?.checked); //here is where we might call a backend service; const user: User = { name: "From sign in", email: emailRef.current!.value, }; setIsLoading(true); setTimeout(() => { setUser(user); setIsLoading(false); navigate("/"); }, 3000); };
核心原因分析
组件卸载导致useEffect未执行
登录逻辑中调用setUser(user)后立即执行navigate("/"),会触发当前登录组件卸载。而useLocalStorage中依赖value变化的useEffect是组件渲染完成后异步执行的,组件卸载后该副作用会被清理,无法完成LocalStorage的写入操作,最终LocalStorage中依然保留初始的null值。getDefaultValue函数的潜在bug
虽然不是当前问题的直接原因,但该函数处理函数类型初始值时存在错误:当initialValue是函数时,仅调用函数但未返回其执行结果,会导致后续useState初始化得到的是函数本身而非预期值,最终JSON.stringify会将函数序列化为null。
解决方案
方案1:修改useLocalStorage,在设置状态时同步写入LocalStorage
将LocalStorage的写入逻辑从useEffect移到状态更新的回调中,确保状态变更时立即持久化,不受组件卸载影响:
function getDefaultValue<T>(key: string, initialValue: T | null): T | null { const storedValue: string | null = localStorage.getItem(key); if (storedValue) { return JSON.parse(storedValue); } // 修复函数类型初始值的处理逻辑 if (initialValue instanceof Function) { return initialValue(); } return initialValue; } export function useLocalStorage<T>( initialValue: T | null, key: string ): [T | null, React.Dispatch<React.SetStateAction<T | null>>] { const [value, setValue] = useState<T | null>( getDefaultValue(key, initialValue) ); // 包装setValue,同步写入LocalStorage const wrappedSetValue = useCallback((newValue: React.SetStateAction<T | null>) => { setValue(prev => { const finalValue = typeof newValue === 'function' ? (newValue as (prev: T | null) => T | null)(prev) : newValue; localStorage.setItem(key, JSON.stringify(finalValue)); return finalValue; }); }, [key]); return [value, wrappedSetValue]; }
方案2:延迟导航,确保副作用执行完成
如果不想修改Hook实现,可以在setUser后延迟执行navigate,给useEffect留足执行时间:
setTimeout(() => { setUser(user); setIsLoading(false); // 延迟导航,确保useEffect执行完成 setTimeout(() => navigate("/"), 100); }, 3000);
注:该方案依赖React的渲染周期,稳定性不如方案1。
内容的提问来源于stack exchange,提问作者Amos Machora
相关产品推荐
相关产品推荐

