React简单示例中刷新时保存状态被覆盖的原因探究
React Todo应用localStorage状态加载后被清空的问题解析
问题背景
跟着教程制作React Todo应用时,遇到从localStorage加载的状态会被异步清空的问题,评论区其他用户也有同样遭遇。虽然找到了临时解决方案,但不清楚背后的原因,也不确定这是不是React的预期行为。
代码与控制台输出
应用代码
import React, { useState, useRef, useEffect } from "react" import TodoList from "./TodoList" import { v4 as uuidv4 } from 'uuid'; const LOCAL_STORAGE_KEY = 'todosApp.todos' function App() { const [todos, setTodos] = useState([]) const todoNameRef = useRef() useEffect(() => { console.log(`useEffect[]`) const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)); if (storedTodos) { console.log(`set todos to: ${JSON.stringify(storedTodos)}`) setTodos(storedTodos) // can't print here - value is set asynchronously // console.log(`loaded todos: ${JSON.stringify(todos)}`) } }, []) useEffect(() => { console.log(`useEffect[todos]: ${JSON.stringify(todos)}`) if (todos.length != 0) { console.log('save') localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos)) } }, [todos]) function toggleTodo(id) { const newTodos = [...todos] const todo = newTodos.find(todo => todo.id === id) console.log(`toggleTodo: ${todo.name}`) todo.complete = !todo.complete setTodos(newTodos) } function handleAddTodo(e) { const name = todoNameRef.current.value console.log(`handleAddTodo: ${name}`) // setTodos(todos.concat({completed: false, name: todoNameRef.current.value})) if (name === '') { return } setTodos([...todos, { id:uuidv4(), name:name, complete:false }]) } return ( <div> <TodoList todos={todos} handleCheckboxChanged={toggleTodo}/> <input ref={todoNameRef} type="text" /> <button onClick={handleAddTodo}>Add Todo</button> <button>Clear Completed</button> <div>0 left to do</div> </div> ) } export default App;
控制台输出
useEffect[] App.js:15 set todos to: [{"id":"77fe1e9e-91aa-4a34-9bfb-b1842ea5518d","name":"asfd","complete":false},{"id":"8dabea66-4ed9-4f10-9003-af1b34b4558a","name":"asfd","complete":false},{"id":"6d4e9350-11cd-4ace-8766-485e1f8817ad","name":"asfd","complete":false}] App.js:23 useEffect[todos]: [] App.js:12 useEffect[] App.js:15 set todos to: [] App.js:23 useEffect[todos]: [] App.js:23 useEffect[todos]: []
原因分析
这是React的预期行为,核心问题出在两个useEffect的执行顺序和localStorage的读写逻辑上:
- 组件初始化时,
todos的初始值是空数组[]。 - 第一个空依赖的
useEffect(仅在组件挂载时执行)读取localStorage中的数据,调用setTodos(storedTodos)更新状态,但setTodos是异步操作,此时组件状态还未完成更新。 - 第二个依赖
todos的useEffect会在组件初始化(todos为空数组)和每次todos变化时执行。如果没有if (todos.length != 0)的判断,初始化阶段就会把空数组写入localStorage,直接覆盖掉之前存储的有效数据。 - 当
setTodos(storedTodos)完成异步更新后,组件重新渲染,但第一个空依赖的useEffect不会再执行(依赖为空数组,仅挂载时执行一次),此时localStorage已经被空数组覆盖,下次刷新页面时读取到的就是空数据,最终导致状态被清空。
解决方案解释
你添加的if (todos.length != 0)判断是关键:
- 组件初始化时,
todos是空数组,条件不满足,不会执行localStorage.setItem,避免了覆盖原有存储数据。 - 只有当
todos真正有内容(添加新todo、修改todo状态)时,才会将数据写入localStorage,保证存储的始终是有效状态。
总结
这不是React的bug,而是需要正确处理useEffect执行时机和状态更新异步性的典型场景。通过添加长度判断,我们避免了初始化阶段的无效写入,确保localStorage中始终保存的是最新的有效状态。
内容的提问来源于stack exchange,提问作者ohthepain
相关产品推荐
相关产品推荐

