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

React中setState修改对象属性导致历史数组全部同步变更问题

解决待办应用历史记录同步修改的问题

你遇到的核心问题是JavaScript对象的引用特性:当你把list存入history时,并没有真正创建独立副本,只是把数组的引用存了进去。数组里的每个待办项也是对象引用,所以你直接修改element.isChecked时,history里所有旧版本的对应对象都会跟着变——因为它们指向的是同一个内存地址。

下面是具体的修复方案和修改后的代码:

关键修复点

  1. 保存历史时做深拷贝:每次往history里存list,都要创建完全独立的副本,断开和原数组/对象的引用关联
  2. 遵循不可变状态原则:不要直接修改原状态对象,而是生成新数组/新对象来更新React状态
  3. 修复Undo函数的数组突变问题:避免直接修改原history数组,用安全的方式截取历史记录

修改后的完整代码

import "./styles.css";
import { useState } from "react";

export default function App() {
  const [input, setInput] = useState("")
  const [list, setList] = useState([
    {key:584535, value:"Do laundry", isChecked: false},
    {key:439735, value:"Take out trash", isChecked: false},
    {key:589774, value:"Do dishes", isChecked: true},
    {key:573735, value:"code", isChecked: false},
    {key:589739, value:"margin", isChecked: false},
    {key:585735, value:"textbox", isChecked: false}]
  )
  // 初始化时存入初始list的深拷贝
  const [history, setHistory] = useState([JSON.parse(JSON.stringify(list))])

  function handleCheckClick(e) {    
    const checkkey = parseInt(e.target.getAttribute("checkboxkey"));
    // 先存当前list的深拷贝到历史记录
    setHistory(prev => [...prev, JSON.parse(JSON.stringify(list))]);
    // 生成新数组,不修改原对象
    const newList = list.map(element => {
      if(element.key === checkkey){
        // 返回新对象修改状态
        return {...element, isChecked: !element.isChecked};
      }
      return element;
    });
    setList(newList);
  }

  function handleRemoveClick(e) {
    const buttonKey = parseInt(e.target.getAttribute("buttonkey"));
    setHistory(prev => [...prev, JSON.parse(JSON.stringify(list))]);
    // 用filter生成新数组,替代直接修改原数组
    const newList = list.filter(element => element.key !== buttonKey);
    setList(newList);
  }

  function handleSubmit() {
    if(input.trim() === ""){return}
    const key = Math.floor(100000 + Math.random() * 900000);
    setHistory(prev => [...prev, JSON.parse(JSON.stringify(list))]);
    setList(prev => [...prev, {key: key, value: input, isChecked:false}]); 
    setInput("");
  }

  function handleHistoryClick(){
    if(history.length <= 1) return; // 保留初始状态,防止undo到空
    // 获取上一个历史版本,同时截取历史记录
    const prevList = history[history.length - 1];
    setHistory(prev => prev.slice(0, -1));
    setList(prevList);
  }

  return (
    <div className="App">
      <h1>To Do</h1>
      <input 
        id="input" 
        type="text" 
        value={input} 
        onChange={(e) => setInput(e.target.value)}
      />
      <button onClick={handleSubmit}>Submit</button>
      <button onClick={handleHistoryClick}>Undo</button>
      <div id="container">
        <ol>
          {list.map(({key, value, isChecked}) => (
            <React.Fragment key={key}>
              {isChecked ? <li id="strike">{value}</li> : <li>{value}</li>}
              <input 
                type="checkbox" 
                checkboxkey={key} 
                onClick={handleCheckClick}
                checked={isChecked} // 让复选框状态和数据同步
              />
              <button buttonkey={key} onClick={handleRemoveClick}>Remove</button>
              <br />
            </React.Fragment>
          ))}
        </ol>
      </div>
    </div>
  );
}

额外优化说明

  • 给复选框加上checked={isChecked},确保UI状态和数据完全同步
  • 把字符串类型的key转为数字,避免类型比较时的潜在问题
  • 用React.Fragment替代<>,代码更规范
  • 限制Undo次数,防止把初始状态也删掉

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 00:27:10