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

React待办列表DOM编辑添加任务异常问题求助

待办列表应用添加任务的问题修复方案

问题根源分析

问题1:中间添加任务时列表截断、新任务重复

  • 额外输入框添加任务时,直接使用闭包中的旧list状态创建新数组,而非通过React状态更新的函数式方式获取最新列表,当列表已发生变化时,旧数组内容并非最新,导致splice操作破坏原有列表结构。
  • 新任务使用固定ID "0000",违反React列表key必须唯一的要求,引发渲染混乱。

问题2:点击前置任务内容消失

  • 事件监听中捕获的item、index等变量来自闭包,列表更新后这些变量仍指向旧状态,修改状态时会操作错误的位置。
  • 原生DOM操作与React状态管理混合,导致DOM与状态不同步,React重新渲染时覆盖了手动修改的DOM内容。

修复方案

1. 统一使用函数式状态更新

所有修改list的操作都采用setList(prevList => {...})形式,确保获取最新的状态快照,彻底规避闭包陷阱。

2. 生成唯一任务ID

添加任务时生成唯一ID,和基础输入框逻辑保持一致,避免key冲突导致的渲染异常。

3. 用React状态控制编辑状态

移除手动创建DOM元素的逻辑,将任务的编辑状态(是否显示输入框)存入任务对象,由React驱动视图渲染,确保DOM与状态完全同步。

4. 简化事件绑定逻辑

不再通过useEffect手动添加监听,直接在JSX中绑定合成事件,利用React的事件系统自动处理监听的添加与移除。

修改后的完整代码

import { useState } from 'react';

function ChecklistEditor() {
  const [list, setList] = useState([]);
  const [inputValue, setInputValue] = useState("");

  // 生成唯一任务ID
  const generateId = () => `task${Math.random() * 10000000000000000}`;

  // 基础输入框添加任务
  const add = (e) => {
    e.preventDefault();
    const trimmedText = inputValue.trim();
    if (trimmedText.length === 0) return;
    
    // 检查重复任务(可根据需求调整)
    const hasDuplicate = list.some(item => item.taskText === trimmedText);
    if (hasDuplicate) return;

    setList(prevList => [
      ...prevList,
      { 
        id: generateId(), 
        taskText: trimmedText, 
        inProgress: true, 
        done: false,
        isEditing: false // 新增编辑状态标识
      }
    ]);
    setInputValue("");
  };

  // 更新任务完成状态
  const setTaskStatus = (id, status) => {
    setList(prevList => {
      const targetId = id.split("-")[0];
      const index = prevList.findIndex(item => item.id === targetId);
      if (index === -1) return prevList;
      return prevList.toSpliced(index, 1, {
        ...prevList[index],
        done: status,
        inProgress: !status
      });
    });
  };

  // 切换任务编辑状态
  const toggleTaskEditing = (id) => {
    setList(prevList => {
      const index = prevList.findIndex(item => item.id === id);
      if (index === -1) return prevList;
      return prevList.toSpliced(index, 1, {
        ...prevList[index],
        isEditing: !prevList[index].isEditing,
        originalText: prevList[index].taskText // 暂存原始文本用于取消编辑
      });
    });
  };

  // 更新任务文本
  const updateTaskText = (id, newText) => {
    const trimmedText = newText.trim();
    if (trimmedText.length === 0) {
      toggleTaskEditing(id);
      return;
    }

    setList(prevList => {
      const index = prevList.findIndex(item => item.id === id);
      if (index === -1) return prevList;
      return prevList.toSpliced(index, 1, {
        ...prevList[index],
        taskText: trimmedText,
        isEditing: false,
        originalText: undefined
      });
    });
  };

  // 在指定任务后插入新任务
  const insertTaskAfter = (id, newTaskText) => {
    const trimmedText = newTaskText.trim();
    if (trimmedText.length === 0) return;

    setList(prevList => {
      const index = prevList.findIndex(item => item.id === id);
      if (index === -1) return prevList;
      const newTask = {
        id: generateId(),
        taskText: trimmedText,
        inProgress: true,
        done: false,
        isEditing: false
      };
      const newList = [...prevList];
      newList.splice(index + 1, 0, newTask);
      return newList;
    });
  };

  return (
    <form className="task-list" onSubmit={add}>
      <ul className="task-list__list">
        {list.map((item) => (
          <li id={item.id} key={item.id} className="task-list__item">
            <div className="task-list__task-container">
              <input
                type="checkbox"
                id={`${item.id}-checkBox`}
                checked={item.done}
                onChange={(e) => setTaskStatus(item.id, e.target.checked)}
                className={`task-list__checkbox ${item.done ? "task-list__checkbox_done" : ""}`}
              />
              {item.isEditing ? (
                <input
                  type="text"
                  value={item.taskText}
                  onChange={(e) => {
                    setList(prevList => {
                      const index = prevList.findIndex(it => it.id === item.id);
                      return prevList.toSpliced(index, 1, {
                        ...prevList[index],
                        taskText: e.target.value
                      });
                    });
                  }}
                  onBlur={() => updateTaskText(item.id, item.taskText)}
                  onKeyDown={(e) => {
                    if (e.code === "Enter") {
                      updateTaskText(item.id, item.taskText);
                      // 按Enter触发插入新任务的逻辑
                      const newText = prompt("输入新任务内容:");
                      if (newText) insertTaskAfter(item.id, newText);
                    } else if (e.code === "Escape") {
                      // 按ESC取消编辑,恢复原始文本
                      setList(prevList => {
                        const index = prevList.findIndex(it => it.id === item.id);
                        return prevList.toSpliced(index, 1, {
                          ...prevList[index],
                          taskText: item.originalText,
                          isEditing: false,
                          originalText: undefined
                        });
                      });
                    }
                  }}
                  autoFocus
                  className="task-list__text-input"
                />
              ) : (
                <span
                  className={`task-list__text checkbox__title ${item.done ? "task-list__text_done" : ""}`}
                  onClick={() => toggleTaskEditing(item.id)}
                >
                  {item.taskText}
                </span>
              )}
            </div>
          </li>
        ))}
      </ul>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="Что хотите добавить в чек-лист?"
        className="task-list__input"
      />
      <button type="submit">添加任务</button>
    </form>
  );
}

关键修复点说明

  • 状态驱动视图:用isEditing状态控制输入框的显示/隐藏,彻底移除原生DOM操作,确保DOM与状态完全同步。
  • 函数式更新:所有setList操作依赖prevList获取最新状态,解决闭包导致的旧状态引用问题。
  • 唯一ID:统一生成唯一ID,避免React列表key冲突引发的渲染异常。
  • 简化事件逻辑:直接在JSX中绑定事件,不再通过useEffect手动管理监听,减少闭包陷阱。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 14:38:10