You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
};

核心原因分析

  1. 组件卸载导致useEffect未执行
    登录逻辑中调用setUser(user)后立即执行navigate("/"),会触发当前登录组件卸载。而useLocalStorage中依赖value变化的useEffect是组件渲染完成后异步执行的,组件卸载后该副作用会被清理,无法完成LocalStorage的写入操作,最终LocalStorage中依然保留初始的null值。

  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 22:50:23