React DND看板拖动任务项后索引未更新问题求助
React DND Kanban看板拖放后索引未更新问题
问题描述
用React DND构建Kanban看板,每个任务项渲染时分配index,拖放后调用setState更新状态,确认状态已更新,但index并未同步更新,导致第一次拖放后无法正常执行后续操作。
相关代码
Board.tsx
return ( <TaskWrapper ref={drop}> <BoardName>{props.groupName}</BoardName> {props.taskList.map((taskItem, i) => { return ( <Draggable key={taskItem.id} task={taskItem} index={props.firstIndex + i} firstIndex={props.firstIndex} swapTaskItem={props.swapTaskItem} > <TaskCard task={taskItem} deleteTask={props.deleteTask} editTask={props.editTask} parentRef={ref} /> </Draggable> ); })}
Draggable.tsx
function Draggable(props: Props) { const ref = useRef<HTMLDivElement>(null); const [{ isDragging, canDrag }, drag] = useDrag(() => ({ type: ItemTypes.TASKITEM, // item: { ...props.task, index: props.index }, item: () => { debugger; return { ...props.task, index: props.index }; }, isDragging: (monitor) => { return monitor.getItem().id === props.task.id; }, collect: (monitor) => ({ isDragging: monitor.isDragging(), canDrag: monitor.canDrag(), }), })); const [, drop] = useDrop({ accept: ItemTypes.TASKITEM, hover(dragItem: TaskItemForDrag, monitor) { if (!ref.current) return; if (dragItem.id === props.task.id) return; debugger; const dragIndex = dragItem.index - props.firstIndex; const hoverIndex = props.index - props.firstIndex; if (!dragIndex || dragIndex === hoverIndex) return; if (props.task.group === dragItem.group) { // Determine rectangle on screen const hoverRect = ref.current?.getBoundingClientRect(); // Determine mouse position const mousePosition = monitor.getClientOffset(); const hoverMiddleY = (hoverRect!.bottom - hoverRect!.top) / 2; const hoverClientY = mousePosition!.y - hoverRect!.top; if (!mousePosition) return; if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return; if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY * 1.5) return; } props.swapTaskItem(dragItem, dragIndex, hoverIndex, props.task.group); dragItem.index = props.index; }, }); drag(drop(ref)); return <div ref={ref}>{props.children}</div>; } export default Draggable;
useTaskList
const swapTaskItem = useCallback( ( dragItem: TaskItem, dragIndex: number, targetIndex: number, hoverGroup: string ) => { setTaskCards((prevState: TaskColumn[]) => { const sourceCard = find(prevState, { groupName: dragItem.group }); const targetCard = find(prevState, { groupName: hoverGroup }); if (targetCard) { const dragItem = targetCard.list[dragIndex]; const newList = targetCard.list.filter( (item, idx) => item !== null && idx !== dragIndex ); if (!dragItem) return prevState; newList.splice(targetIndex, 0, dragItem); const updatedItems = { ...targetCard, list: newList }; debugger; return prevState.map((item) => item.groupName === updatedItems.groupName ? updatedItems : item ); } return prevState; }); }, [setTaskCards] );
Task.d.ts
export type TaskItem = { id: string; name: string; group: string; }; export type TaskColumn = { groupID: string; groupName: string; list: TaskItem[]; }; export type TaskItemForDrag = TaskItem & { index: number }; export type Tasks = { tasks: TaskCard[]; };
问题根因
- 拖拽快照不会自动更新:React DND在拖拽开始时会创建
item的快照并存储在内部monitor中,后续组件重新渲染、props.index更新时,这个快照不会同步更新。你在hover里修改dragItem.index只是修改了内存临时对象,无法同步到组件状态或下一次拖拽的item中。 - 索引使用逻辑混乱:
swapTaskItem内部重新声明了dragItem变量,覆盖了传入的参数,容易导致逻辑混淆;同时拖拽过程中依赖的是拖拽开始时的旧索引,而非重新渲染后的最新索引。 - item函数仅在拖拽开始时执行:
item: () => {...}虽然能拿到拖拽开始时的最新索引,但拖拽过程中组件重渲染时该函数不会再次执行,monitor中的item始终是旧索引值。
解决办法
1. 移除索引依赖,用任务ID动态计算位置
索引是渲染衍生值,不应存储在拖拽item中,每次需要时通过任务ID在列表中动态查找:
- 修改
Draggable.tsx的useDrag配置:
const [{ isDragging, canDrag }, drag] = useDrag(() => ({ type: ItemTypes.TASKITEM, item: { id: props.task.id, group: props.task.group }, // 仅传递唯一标识 isDragging: (monitor) => { return monitor.getItem().id === props.task.id; }, collect: (monitor) => ({ isDragging: monitor.isDragging(), canDrag: monitor.canDrag(), }), }), [props.task]); // 添加依赖,任务变化时重新创建拖拽上下文
- 在
hover回调中动态计算索引:
hover(dragItem: {id: string, group: string}, monitor) { if (!ref.current) return; if (dragItem.id === props.task.id) return; // 通过ID查找拖拽项和hover项在当前列表中的索引 const dragIndex = props.taskList.findIndex(item => item.id === dragItem.id); const hoverIndex = props.taskList.findIndex(item => item.id === props.task.id); if (dragIndex === -1 || hoverIndex === -1 || dragIndex === hoverIndex) return; // 后续逻辑保持不变 props.swapTaskItem(dragItem, dragIndex, hoverIndex, props.task.group); }
注意:需要给
Draggable组件传入当前看板的taskList作为props,用于动态计算索引。
2. 修复swapTaskItem的变量覆盖与状态更新逻辑
移除参数覆盖,优化状态更新的不可变性:
const swapTaskItem = useCallback( ( dragItemInfo: {id: string, group: string}, dragIndex: number, targetIndex: number, hoverGroup: string ) => { setTaskCards((prevState) => { const sourceColumn = prevState.find(col => col.groupName === dragItemInfo.group); const targetColumn = prevState.find(col => col.groupName === hoverGroup); if (!sourceColumn || !targetColumn) return prevState; // 从源列取出拖拽项 const [draggedTask] = sourceColumn.list.splice(dragIndex, 1); // 插入到目标列的对应位置 targetColumn.list.splice(targetIndex, 0, draggedTask); // 返回新状态数组,确保修改过的列是新对象 return prevState.map(col => { if (col.groupName === dragItemInfo.group) return {...sourceColumn}; if (col.groupName === hoverGroup) return {...targetColumn}; return col; }); }); }, [] // 函数式更新无需依赖setTaskCards );
3. 给useDrag和useDrop添加依赖数组
确保props变化时,拖拽/投放上下文同步更新:
// 在Draggable的useDrop中添加依赖 const [, drop] = useDrop({ // 原有配置不变 }, [props.task, props.taskList, props.swapTaskItem]);
内容的提问来源于stack exchange,提问作者user56963
相关产品推荐
相关产品推荐

