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

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的读写逻辑上:

  1. 组件初始化时,todos的初始值是空数组[]。
  2. 第一个空依赖的useEffect(仅在组件挂载时执行)读取localStorage中的数据,调用setTodos(storedTodos)更新状态,但setTodos是异步操作,此时组件状态还未完成更新。
  3. 第二个依赖todos的useEffect会在组件初始化(todos为空数组)和每次todos变化时执行。如果没有if (todos.length != 0)的判断,初始化阶段就会把空数组写入localStorage,直接覆盖掉之前存储的有效数据。
  4. 当setTodos(storedTodos)完成异步更新后,组件重新渲染,但第一个空依赖的useEffect不会再执行(依赖为空数组,仅挂载时执行一次),此时localStorage已经被空数组覆盖,下次刷新页面时读取到的就是空数据,最终导致状态被清空。

解决方案解释

你添加的if (todos.length != 0)判断是关键:

  • 组件初始化时,todos是空数组,条件不满足,不会执行localStorage.setItem,避免了覆盖原有存储数据。
  • 只有当todos真正有内容(添加新todo、修改todo状态)时,才会将数据写入localStorage,保证存储的始终是有效状态。

总结

这不是React的bug,而是需要正确处理useEffect执行时机和状态更新异步性的典型场景。通过添加长度判断,我们避免了初始化阶段的无效写入,确保localStorage中始终保存的是最新的有效状态。

内容的提问来源于stack exchange,提问作者ohthepain

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 05:09:35