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

Next.js项目中使用dnd-kit拖拽排序时,占位符释放元素后立即消失而非等待DragOverlay过渡完成的问题

解决dnd-kit拖拽释放时占位符过早消失的UI生硬跳转问题

这个问题我之前也踩过坑,核心原因是咱们在onDragEnd里立刻把activeId设为null,导致占位符瞬间消失,但DragOverlay的过渡动画还在执行,视觉上就出现了突兀的跳转。要实现你想要的流畅效果,关键是让占位符保持可见,直到DragOverlay的过渡动画完全结束。

下面是具体的修改方案,我会一步步解释逻辑:

1. 新增过渡状态跟踪

首先,我们需要一个状态来标记DragOverlay是否正在执行过渡动画:

const [isTransitioning, setIsTransitioning] = useState(false);

2. 调整拖拽结束/取消的逻辑

在handleDragEnd里,不要立刻清除activeId,而是先开启过渡状态(因为此时DragOverlay的动画刚启动):

function handleDragEnd(event: any) {
  const { active, over } = event;
  if (over && active.id !== over.id) {
    const oldIndex = shelves.findIndex((s) => s.id === active.id);
    const newIndex = shelves.findIndex((s) => s.id === over.id);
    setShelves((items) => arrayMove(items, oldIndex, newIndex));
  }
  // 不立刻清除activeId,先标记过渡开始
  setIsTransitioning(true);
}

同时,拖拽取消的场景不需要等待动画,直接清除所有状态:

onDragCancel={() => {
  setActiveId(null);
  setIsTransitioning(false);
}}

3. 监听DragOverlay的过渡结束事件

给OverlayItem添加onTransitionEnd事件,当动画完成后再清理状态,让占位符消失:

function OverlayItem({ shelf, onTransitionEnd }: { shelf: Shelf; onTransitionEnd: () => void }) {
  return (
    <div 
      className="flex items-center gap-2 p-3 rounded-md border border-blue-400 bg-blue-50 shadow-lg w-80"
      onTransitionEnd={onTransitionEnd}
    >
      <span className="px-2 py-1 rounded bg-gray-200">☰</span>
      <span>{shelf.name}</span>
    </div>
  );
}

然后在使用OverlayItem时传入这个回调:

<DragOverlay>
  {activeShelf ? (
    <OverlayItem 
      shelf={activeShelf} 
      onTransitionEnd={() => {
        // 过渡完成后清除状态,占位符随之消失
        setActiveId(null);
        setIsTransitioning(false);
      }} 
    />
  ) : null}
</DragOverlay>

4. 保持占位符的显示逻辑

原来的isPlaceholder判断不需要修改,因为activeId会一直保留到过渡结束,所以占位符会持续显示在被拖拽元素的目标位置:

<SortableItem 
  key={shelf.id} 
  shelf={shelf} 
  selectedShelfs={selectedShelfs} 
  setSelectedShelfs={setSelectedShelfs} 
  selectedShelfId={selectedShelfId} 
  setSelectedShelfId={setSelectedShelfId} 
  isPlaceholder={activeId === shelf.id} 
/>

修改后的完整代码

"use client";
import React, { useState } from "react";
import { 
  DndContext, 
  closestCenter, 
  PointerSensor, 
  useSensor, 
  useSensors, 
  DragOverlay, 
} from "@dnd-kit/core";
import { 
  SortableContext, 
  useSortable, 
  arrayMove, 
  verticalListSortingStrategy, 
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";

type Shelf = {
 id: number;
 name: string;
};

const initialShelves: Shelf[] = [
 { id: 1, name: "Shelf A" },
 { id: 2, name: "Shelf B" },
 { id: 3, name: "Shelf C" },
];

function SortableItem({ shelf, selectedShelfs, setSelectedShelfs, selectedShelfId, setSelectedShelfId, isPlaceholder, }: any) {
 const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: shelf.id });
 const style = {
 transform: CSS.Transform.toString(transform),
 transition,
 };
 return (
 <li ref={setNodeRef} style={style}>
 {isPlaceholder ? (
 <div className="p-3 mb-2 rounded-md bg-red-600 border border-red-700 h-[44px]" />
 ) : (
 <div className={` flex items-center gap-2 p-3 mb-2 rounded-md border text-black bg-white ${selectedShelfId === shelf.id ? "border-blue-500 border-2" : "border-gray-300"} `} >
 <span {...attributes} {...listeners} className="cursor-grab select-none px-2 py-1 rounded bg-gray-200" > ☰ </span>
 <input type="checkbox" checked={selectedShelfs.includes(shelf.id)} onChange={() => setSelectedShelfs((prev: number[]) => prev.includes(shelf.id) ? prev.filter((id) => id !== shelf.id) : [...prev, shelf.id], ) } />
 <div className="flex-1 cursor-pointer" onClick={() => setSelectedShelfId(shelf.id)} > {shelf.name} </div>
 </div>
 )}
 </li>
 );
}

function OverlayItem({ shelf, onTransitionEnd }: { shelf: Shelf; onTransitionEnd: () => void }) {
 return (
 <div 
 className="flex items-center gap-2 p-3 rounded-md border border-blue-400 bg-blue-50 shadow-lg w-80"
 onTransitionEnd={onTransitionEnd}
 >
 <span className="px-2 py-1 rounded bg-gray-200">☰</span>
 <span>{shelf.name}</span>
 </div>
 );
}

export default function Home() {
 const [shelves, setShelves] = useState(initialShelves);
 const [selectedShelfs, setSelectedShelfs] = useState<number[]>([]);
 const [selectedShelfId, setSelectedShelfId] = useState<number | null>(null);
 const [activeId, setActiveId] = useState<number | null>(null);
 const [isTransitioning, setIsTransitioning] = useState(false);
 const sensors = useSensors(useSensor(PointerSensor));

 function handleDragEnd(event: any) {
 const { active, over } = event;
 if (over && active.id !== over.id) {
 const oldIndex = shelves.findIndex((s) => s.id === active.id);
 const newIndex = shelves.findIndex((s) => s.id === over.id);
 setShelves((items) => arrayMove(items, oldIndex, newIndex));
 }
 setIsTransitioning(true);
 }

 const activeShelf = shelves.find((s) => s.id === activeId);

 return (
 <div className="max-w-md mx-auto mt-10 font-sans">
 <h3 className="mb-4 text-lg font-semibold">Shelves</h3>
 <DndContext 
 sensors={sensors} 
 collisionDetection={closestCenter} 
 onDragStart={(e) => setActiveId(e.active.id as number)} 
 onDragEnd={handleDragEnd} 
 onDragCancel={() => {
   setActiveId(null);
   setIsTransitioning(false);
 }} 
 >
 <SortableContext items={shelves.map((s) => s.id)} strategy={verticalListSortingStrategy} >
 <ul className="list-none p-0">
 {shelves.map((shelf) => (
 <SortableItem 
 key={shelf.id} 
 shelf={shelf} 
 selectedShelfs={selectedShelfs} 
 setSelectedShelfs={setSelectedShelfs} 
 selectedShelfId={selectedShelfId} 
 setSelectedShelfId={setSelectedShelfId} 
 isPlaceholder={activeId === shelf.id} 
 />
 ))}
 </ul>
 </SortableContext>
 <DragOverlay>
 {activeShelf ? (
 <OverlayItem 
 shelf={activeShelf} 
 onTransitionEnd={() => {
   setActiveId(null);
   setIsTransitioning(false);
 }} 
 />
 ) : null}
 </DragOverlay>
 </DndContext>
 </div>
 );
}

逻辑说明

  1. 延迟清除activeId:拖拽结束后,activeId会保留到过渡动画完成,所以占位符会一直显示在被拖拽元素的目标位置(因为我们已经更新了shelves的顺序)。
  2. 过渡结束后清理状态:当DragOverlay的动画完全结束,再清除activeId,此时占位符刚好和DragOverlay的位置重合,消失时不会有视觉跳转。
  3. 兼容取消拖拽场景:如果用户中途取消拖拽,直接清除所有状态,避免不必要的等待。

这样修改后,就能实现你想要的流畅效果:拖拽时显示占位符,释放后DragOverlay平滑过渡到目标位置,动画完成后占位符自然消失。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 06:42:36