最近在做一个部署日志功能,需要把日志实时存入 IndexedDB,在开发中遇到了一个 Bug:明明数据已经存进了数据库,但只要一刷新页面,缓存数据就莫名其妙被抹除了。
1. 现象描述
逻辑很简单:
useState初始化一个log字符串。- 一个
useEffect监听log变化,只要有更新就save到本地数据库。 - 另一个
useEffect在组件挂载时,从数据库get出数据并setLog。
Bug:刷新页面,控制台数据库里曾有过数据,但瞬间变成了空字符串。
2. 为什么数据被抹除了?
问题出在 React 组件的生命周期和异步操作的时间差上:
- 第一步(初始化): 组件挂载,
log的初始状态是''(空字符串)。 - 第二步(执行监听逻辑): 监听
log的useEffect发现log发生了变化(初始化)。此时,从数据库读取旧数据的异步操作还没完成,这个 Effect 就拿着初始值''去执行了保存,把数据库里的旧数据覆盖了。 - 第三步(读取结束): 异步读取终于拿到了结果,但此时数据库里已经是刚才被覆盖掉的空字符串了。
3. 解决方法:给保存逻辑加状态锁
我们需要确保在数据从数据库恢复到 State 之前,严禁往数据库写回任何数据。
这里适合用 useRef 来做一个“初始化锁”。
修正后的逻辑:
const [log, setLog] = useState('');
const inited = useRef(false); // 初始化锁
// 挂载时异步读取
useEffect(() => {
db.getCache().then(res => {
setLog(res || '');
// 读取完成并赋值后,取消锁 (标记为已初始化)
inited.current = true;
});
}, []);
// 监听变化并保存
useEffect(() => {
// 如果锁没取消 (还在初始化),直接跳过保存
if (!inited.current) return;
saveToDB(log);
}, [log]);
4. 为什么用 useRef 而不是 useState?
ref.current的修改是立即生效的,而useState的更新是异步的。在处理生命周期边缘的逻辑时,同步的标记位更安全。- 修改
ref不会触发组件重新渲染。