React任务组件多选框批量操作异常:仅最后一项位置变更的修复
问题描述
开发了一个Task组件,其中handleCheckboxChange函数实现以下功能:
- 勾选任务后,3秒内该任务会移至Done列表
- 取消勾选时,任务会移至Todo列表顶部
单独操作单个任务时功能正常,但同时勾选或取消勾选多个任务时,仅有最后一个任务的位置会发生变化。
问题根源
问题出在handleCheckboxChange函数的延迟更新逻辑中:
- 闭包捕获旧值:
setTimeout内部代码捕获了事件触发时的todos状态、数组索引等变量,3秒后执行时,所有延迟操作都会基于同一个旧状态修改,后面的操作会直接覆盖前面的结果。 - 依赖不稳定的数组索引:用数组索引定位任务,当其他任务移动后,原索引对应的任务可能已经变化,导致操作目标错误。
- 非函数式状态更新:直接使用当前
todos状态进行更新,没有利用React的函数式更新获取最新状态。
修复方案
步骤1:为任务添加唯一ID
为每个任务添加唯一标识,避免依赖不稳定的数组索引。修改初始数据和新增任务逻辑:
Body组件修改:
import React, { useEffect, useState } from 'react' import TodoContainer from './TodoContainer' export default function Body() { const [todos, setTodos] = useState([]); const handleDeleteTask = (taskId, section) => { setTodos(prevTodos => { const updatedTodos = { ...prevTodos }; updatedTodos[section] = prevTodos[section].filter(task => task.id !== taskId); return updatedTodos; }); }; const handleAddTask = (newTask, section) => { setTodos(prevTodos => { const newTaskWithId = { ...newTask, id: Date.now() }; const updatedTodos = { ...prevTodos }; updatedTodos[section] = [newTaskWithId, ...updatedTodos[section]]; return updatedTodos; }); }; useEffect(() => { const hasInitialDataLS = localStorage.getItem("hasInitialDataLS"); if (!hasInitialDataLS) { const preSavedData = { todo: [ { id: 1, title: 'Start with meditation, exercise & breakfast for a productive day', check: 0 }, { id: 2, title: 'Read to learn something new every day', check: 0 }, { id: 3, title: 'Learn something fresh & relevant', check: 0 } ], doing: [ { id: 4, title: 'Engage & question in meetings', check: 0 }, { id: 5, title: 'Use time-blocking for effective days', check: 0 } ], done: [ { id: 6, title: 'Use time-blocking for effective days', check: 1 }, { id: 7, title: 'Congratulate yourself for incorporating healthier habits into your lifestyle, like regular exercise or mindful eating', check: 1 } ] }; localStorage.setItem("todos", JSON.stringify(preSavedData)); localStorage.setItem("hasInitialDataLS", true); setTodos(preSavedData); } if (hasInitialDataLS) { const storeTodos = localStorage.getItem("todos"); if (storeTodos) { setTodos(JSON.parse(storeTodos)); } } }, []); useEffect(() => { localStorage.setItem("todos", JSON.stringify(todos)); }, [todos]); return ( <div className="body"> <TodoContainer className="todo" title="Todo" requirementTasks={todos.todo} onDeleteTask={(taskId) => handleDeleteTask(taskId, 'todo')} setTodos={setTodos} todos={todos} onAddTask={(newTask) => handleAddTask(newTask, 'todo')} /> <TodoContainer className="doing" title="Doing 💪" requirementTasks={todos.doing} onDeleteTask={(taskId) => handleDeleteTask(taskId, 'doing')} setTodos={setTodos} todos={todos} onAddTask={(newTask) => handleAddTask(newTask, 'doing')} /> <TodoContainer className="done" title="Done 🎉" requirementTasks={todos.done} onDeleteTask={(taskId) => handleDeleteTask(taskId, 'done')} setTodos={setTodos} todos={todos} /> </div> ) }
步骤2:修改TodoContainer组件
使用任务id作为key,并传递id给删除函数:
import React, { useState, useEffect, useRef } from 'react' import Task from './Task' import Button from './Button'; import { useDrop } from 'react-dnd'; import { ItemTypes } from './constants'; export default function TodoContainer({ className, title, requirementTasks, onDeleteTask, setTodos, todos, onAddTask }) { const [isDraggingOver, setIsDraggingOver] = useState(false); const ref = useRef(); const [{ isOver }, drop] = useDrop({ accept: ItemTypes.TASK, drop: (item) => { const { taskId, section: originalSection } = item; const newSection = className; if (originalSection !== newSection) { setTodos((prevTodos) => { const taskToMove = prevTodos[originalSection].find(task => task.id === taskId); if (!taskToMove) return prevTodos; const updatedOriginalTasks = prevTodos[originalSection].filter(task => task.id !== taskId); const updatedNewTasks = [ { ...taskToMove, check: className === 'done' ? 1 : 0 }, ...prevTodos[newSection] ]; return ({ ...prevTodos, [originalSection]: updatedOriginalTasks, [newSection]: updatedNewTasks, }); }); } }, collect: (monitor) => ({ isOver: !!monitor.isOver(), }), }); useEffect(() => { drop(ref); const handleDragOver = (event) => { event.preventDefault(); setIsDraggingOver(true); }; const handleDragLeave = () => { setIsDraggingOver(false); }; ref.current.addEventListener("dragover", handleDragOver); ref.current.addEventListener("dragleave", handleDragLeave); return () => { ref.current.removeEventListener("dragover", handleDragOver); ref.current.removeEventListener("dragleave", handleDragLeave); }; }, [drop]); const tasks = requirementTasks || []; return ( <div className={`${className} todo-container ${isDraggingOver ? "dragging-over" : ""}`} ref={ref}> <div className="todo-header"> <h3>{title}</h3> <small>{tasks.length} Tasks</small> </div> {tasks.map((task) => ( <div className="tasks" key={task.id}> <Task context={task.title} check={task.check} handleDeleteTask={() => onDeleteTask(task.id)} taskId={task.id} setTodos={setTodos} todos={todos} requirementTasks={requirementTasks} onAddTask={onAddTask} section={className} /> </div> ))} {className === 'done' ? '' : <Button type='add' onClickFun={() => { onAddTask({ title: '', check: 0 }); }} />} </div> ) }
步骤3:修复Task组件的handleCheckboxChange
使用函数式状态更新,基于最新状态操作,并用id定位任务:
import React, { useState, useRef, useEffect } from 'react' import { useDrag } from 'react-dnd'; import { ItemTypes } from './constants'; import Button from './Button'; export default function Task({ context, check, handleDeleteTask, taskId, setTodos, todos, requirementTasks, section }) { const [, drag] = useDrag({ type: ItemTypes.TASK, item: { taskId, section }, }); const [isEditing, setIsEditing] = useState(false) const [text, setText] = useState(context) const textareaRef = useRef(null) const [isChecked, setIsChecked] = useState(check); const handleSpanClick = () => { setIsEditing(true) } const handleTextChange = (event) => { setText(event.target.value) setTodos(prevTodos => { const updatedTasks = prevTodos[section].map(task => task.id === taskId ? { ...task, title: event.target.value } : task ); return { ...prevTodos, [section]: updatedTasks, }; }); } const handleBlur = () => { setIsEditing(false) } const handleCheckboxChange = () => { const newCheckedState = !isChecked; setIsChecked(newCheckedState); const targetSection = newCheckedState ? "done" : "todo"; const currentSection = section; setTimeout(() => { setTodos(prevTodos => { const taskToMove = prevTodos[currentSection].find(task => task.id === taskId); if (!taskToMove) return prevTodos; const updatedCurrentSection = prevTodos[currentSection].filter(task => task.id !== taskId); const updatedTargetSection = [ { ...taskToMove, check: newCheckedState ? 1 : 0 }, ...prevTodos[targetSection] ]; return { ...prevTodos, [currentSection]: updatedCurrentSection, [targetSection]: updatedTargetSection }; }); }, 3000); }; useEffect(() => { if (isEditing && textareaRef.current) { const textarea = textareaRef.current const textLength = textarea.value.length textarea.setSelectionRange(textLength, textLength) textarea.focus() } }, [isEditing]) return ( <div className={`task ${isEditing ? '' : 'dis-flex'}`} ref={drag}> <label className="checkbox-container"> <input type="checkbox" checked={isChecked} onChange={handleCheckboxChange} /> <span className={`checkmark ${isChecked ? 'checked' : ''}`}></span> </label> {isEditing ? ( <textarea ref={textareaRef} value={text} onChange={handleTextChange} onBlur={handleBlur} className="task-textarea" /> ) : ( <span onClick={handleSpanClick} className={`${isChecked ? 'text-decoration' : ''}`}>{text ? text : (<input placeholder="new Task" className='new-task'></input>)}</span> )} <Button type='delete' onClickFun={handleDeleteTask} /> </div> ) }
修复说明
- 唯一ID标识:用任务id代替数组索引,确保任务定位不受数组顺序变化影响,从根源避免索引失效问题。
- 函数式状态更新:所有状态更新都使用
setTodos(prevTodos => {...}),确保每次操作都基于最新的状态,彻底解决闭包捕获旧值导致的覆盖问题。 - 独立延迟操作:每个任务的延迟更新都独立处理自身任务,不再依赖触发时的旧状态,多个任务操作互不干扰。
内容的提问来源于stack exchange,提问作者sadegh
相关产品推荐
相关产品推荐

