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

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[];
};

问题根因

  1. 拖拽快照不会自动更新:React DND在拖拽开始时会创建item的快照并存储在内部monitor中,后续组件重新渲染、props.index更新时,这个快照不会同步更新。你在hover里修改dragItem.index只是修改了内存临时对象,无法同步到组件状态或下一次拖拽的item中。
  2. 索引使用逻辑混乱:swapTaskItem内部重新声明了dragItem变量,覆盖了传入的参数,容易导致逻辑混淆;同时拖拽过程中依赖的是拖拽开始时的旧索引,而非重新渲染后的最新索引。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 17:05:22