使用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
相关产品推荐
相关产品推荐

