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

使用dnd-kit拖拽时元素突然复位且isDragging变为false的问题

问题:dnd-kit实现跨列表拖拽时元素突然回位,isDragging被设为false

我在React项目中用dnd-kit做了两个任务列表,支持列表内排序和跨列表移动元素,但拖拽过程中元素会突然回到原始位置,同时isDragging状态被设为false。已经查过官方文档,代码逻辑看着没问题,不知道问题出在哪。


相关代码

BoardSectionList组件

import React, {useState} from 'react';
import {
  useSensors,
  useSensor,
  PointerSensor,
  KeyboardSensor,
  DndContext,
  closestCorners,
  DragEndEvent,
  DragStartEvent,
  DragOverEvent,
  DragOverlay,
  DropAnimation,
  defaultDropAnimation,
  defaultDropAnimationSideEffects,
} from '@dnd-kit/core';
import {sortableKeyboardCoordinates, arrayMove} from '@dnd-kit/sortable';
import colors from 'colors.js';
import {getTaskById} from './utilsTasks';
import {findBoardSectionContainer} from './utilsBoard';
import BoardSection from './BoardSection';
import TaskItem from './TaskItem';
import Header from './Header';
import {Task} from './types';

interface BoardSectionListState {
  available: Task[],
  selected: Task[],
}

interface BoardSectionListProps {
  columns: Task[],
}

const BoardSectionList = (props: BoardSectionListProps) => {
  const [boardSections, setBoardSections] = 
    useState<BoardSectionListState>({
      available: [],
      selected: props.columns,
    });

  const [activeTaskId, setActiveTaskId] = useState<null | string>(null);

  const sensors = useSensors(
    useSensor(PointerSensor),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    })
  );

  const handleDragStart = ({active}: DragStartEvent) => {
    setActiveTaskId(active.id as string);
  };

  const handleDragOver = ({active, over}: DragOverEvent) => {
    const activeContainer = findBoardSectionContainer(
      boardSections,
      active.id as string
    );
    const overContainer = findBoardSectionContainer(
      boardSections,
      over?.id as string
    );

    if (
      !activeContainer ||
      !overContainer ||
      activeContainer === overContainer
    ) {
      return;
    }

    setBoardSections((boardSection) => {
      const activeItems = boardSection[activeContainer];
      const overItems = boardSection[overContainer];

      const activeIndex = activeItems.findIndex(
        (item) => item.name === active.id
      );
      const overIndex = overItems.findIndex((item) => item.name !== over?.id);

      return {
        ...boardSection,
        [activeContainer]: [
          ...boardSection[activeContainer].filter(
            (item) => item.name !== active.id
          ),
        ],
        [overContainer]: [
          ...boardSection[overContainer].slice(0, overIndex),
          boardSections[activeContainer][activeIndex],
          ...boardSection[overContainer].slice(
            overIndex,
            boardSection[overContainer].length
          ),
        ],
      };
    });
  };

  const handleDragEnd = ({active, over}: DragEndEvent) => {
    const activeContainer = findBoardSectionContainer(
      boardSections,
      active.id as string
    );
    const overContainer = findBoardSectionContainer(
      boardSections,
      over?.id as string
    );

    if (
      !activeContainer ||
      !overContainer ||
      activeContainer !== overContainer
    ) {
      return;
    }

    const activeIndex = boardSections[activeContainer].findIndex(
      (task) => task.name === active.id
    );
    const overIndex = boardSections[overContainer].findIndex(
      (task) => task.name === over?.id
    );
    if (activeIndex !== overIndex) {
      setBoardSections((boardSection) => ({
        ...boardSection,
        [overContainer]: arrayMove(
          boardSection[overContainer],
          activeIndex,
          overIndex
        ),
      }));
    }

    setActiveTaskId(null);
  };

  const filterColumns = (value) => {
    const updatedAvailableColumns = boardSections.available.map(
      (column) => {
        if (value && column.name.toLowerCase().includes(value.toLowerCase())) {
          return {...column, hide: false};
        }
        return value ? {...column, hide: true} : {...column, hide: false};
      }
    );
    setBoardSections({...boardSections, available: [...updatedAvailableColumns]});
  };

  const selectAll = () => {
    setBoardSections({selected: [...props.columns], available: []});
  };
  const unSelectAll = () => {
    setBoardSections({available: [...props.columns], selected: []});
  };

  const dropAnimation: DropAnimation = {
   ...defaultDropAnimation,
  };

  const task = activeTaskId ? getTaskById(props.columns, activeTaskId) : null;
  return (
    <div style={{height: '90%'}}>
      <Header filterColumns={filterColumns} selectAll={selectAll}  unSelectAll={unSelectAll}/>
      <DndContext
        sensors={sensors}
        collisionDetection={closestCorners}
        onDragStart={handleDragStart}
        onDragOver={handleDragOver}
        onDragEnd={handleDragEnd}
      >
        <div style={{display: 'flex', flexDirection: 'row', height: '79%'}}>
          <BoardSection
            key="available"
            borderRight={{borderRight: `1px ${colors.gray} solid`}}
            id="available"
            tasks={boardSections.available} />
          <BoardSection
            key="selected"
            id="selected"
            tasks={boardSections.selected} />
          <DragOverlay dropAnimation={dropAnimation}>
            {task ? <TaskItem task={task} isDragging /> : null}
          </DragOverlay>
        </div>
      </DndContext>
    </div>
  );
};

export default BoardSectionList;

BoardSection组件

import React, {useMemo} from 'react';
import {map} from 'lodash';
import {useDroppable} from '@dnd-kit/core';
import {
  SortableContext,
  verticalListSortingStrategy,
} from '@dnd-kit/sortable';

import {Task} from './types';
import TaskItem from './TaskItem';
import SortableTaskItem from './SortableTaskItem';

type BoardSectionProps = {
  id: string;
  tasks: Task[];
  borderRight?: Object,
};

const BoardSection = ({id, tasks, borderRight}: BoardSectionProps) => {
  const  {setNodeRef} = useDroppable({
    id,
  });
  const items = useMemo(() => (tasks.map(item => item.name)), [map(tasks, 'name')]);
  return (
    <div style={{boxSizing: 'border-box', padding: '10px', maxWidth: '50%', flexBasis: '50%', height: '100%', overflow: 'auto', ...borderRight}}>
      <SortableContext
        id={id}
        items={items}
        strategy={verticalListSortingStrategy}
      >
        <div ref={setNodeRef}>
          {tasks.map((task) => {
            return !task.hide ? (
              <div key={task.name}>
                <SortableTaskItem id={task.name}>
                  <TaskItem task={task} />
                </SortableTaskItem>
              </div>
            ) : null;
          })}
        </div>
      </SortableContext>
    </div>
  );
};

export default BoardSection;

SortableTaskItem组件

import React from 'react';
import {useSortable} from '@dnd-kit/sortable';
import {CSS} from '@dnd-kit/utilities';

type SortableTaskItemProps = {
  children: React.ReactElement;
  id: string;
};

const SortableTaskItem = ({children, id}: SortableTaskItemProps) => {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({id});

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? .3 : 1,
  };
  return (
    <div ref={setNodeRef} style={style}>
      {children && React.cloneElement(children, {attributes, listeners})}
    </div>
  );
};

export default SortableTaskItem;

问题排查与修复

1. 修复handleDragOver的闭包错误

在跨列表移动的state更新逻辑中,你直接引用了外部的boardSections(旧状态)来获取要移动的元素,这会导致状态更新异常。改用回调函数内的当前状态值:

setBoardSections((boardSection) => {
  // ...省略其他代码
  // 改用回调内的boardSection获取当前状态的元素
  const movedItem = boardSection[activeContainer][activeIndex];
  
  return {
    ...boardSection,
    [activeContainer]: [
      ...boardSection[activeContainer].filter(item => item.name !== active.id),
    ],
    [overContainer]: [
      ...boardSection[overContainer].slice(0, overIndex),
      movedItem,
      ...boardSection[overContainer].slice(overIndex),
    ],
  };
});

2. 修正BoardSection的useMemo依赖项

useMemo的依赖项用了map(tasks, 'name'),这会导致每次渲染都生成新数组,触发SortableContext频繁更新,破坏拖拽状态。改为直接依赖tasks:

const items = useMemo(() => tasks.map(item => item.name), [tasks]);

3. 修复handleDragEnd的容器判断逻辑

handleDragEnd中基于旧的boardSections判断容器会出错,改用函数式更新获取最新状态,同时调整逻辑只处理同列表排序:

const handleDragEnd = ({active, over}: DragEndEvent) => {
  setBoardSections((currentSections) => {
    const activeContainer = findBoardSectionContainer(currentSections, active.id as string);
    const overContainer = findBoardSectionContainer(currentSections, over?.id as string);

    if (!activeContainer || !overContainer) return currentSections;

    // 仅处理同列表排序
    if (activeContainer === overContainer) {
      const activeIndex = currentSections[activeContainer].findIndex(task => task.name === active.id);
      const overIndex = currentSections[overContainer].findIndex(task => task.name === over?.id);
      
      if (activeIndex !== overIndex) {
        return {
          ...currentSections,
          [overContainer]: arrayMove(currentSections[overContainer], activeIndex, overIndex),
        };
      }
    }
    return currentSections;
  });

  setActiveTaskId(null);
};

4. 确认findBoardSectionContainer函数正确性

确保该函数能遍历当前的available和selected列表,正确根据taskId找到对应的容器,避免跨列表移动后无法定位元素所在容器。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 12:07:31