Next.js项目中使用dnd-kit实现侧边栏拖拽排序:Placeholder在释放元素时立即消失而非等待DragOverlay过渡完成的问题
解决dnd-kit拖拽释放时Placeholder生硬消失的问题
我明白你遇到的问题——拖拽释放瞬间Placeholder立刻消失,而DragOverlay还在执行归位动画,导致UI跳变。核心原因是你在handleDragEnd里立刻清空了activeId,让Placeholder提前消失了。我们要做的就是让Placeholder等到DragOverlay的动画完全结束后再消失,下面是具体的解决方案:
核心思路
- 不再在拖拽结束时立刻清空
activeId,而是先标记"过渡中"状态 - 监听DragOverlay的动画结束事件,等动画完成后再清空
activeId、隐藏Placeholder - 确保拖拽取消的场景也遵循同样的逻辑
具体修改步骤
1. 添加过渡状态与Ref
在Home组件中新增状态跟踪过渡状态,同时给DragOverlay元素添加Ref用于监听动画:
const [isTransitioning, setIsTransitioning] = useState(false); const overlayRef = useRef<HTMLDivElement>(null);
2. 监听DragOverlay的动画结束事件
通过transitionend事件精准捕捉动画完成时机,此时再重置状态:
useEffect(() => { const handleTransitionEnd = () => { if (isTransitioning) { // 动画结束后才清空activeId,让Placeholder消失 setActiveId(null); setIsTransitioning(false); } }; const element = overlayRef.current; if (element) { element.addEventListener('transitionend', handleTransitionEnd); } // 组件卸载时清理监听 return () => { if (element) { element.removeEventListener('transitionend', handleTransitionEnd); } }; }, [isTransitioning]);
3. 修改拖拽结束与取消的逻辑
不再直接清空activeId,而是先标记过渡状态:
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让Placeholder继续显示 setIsTransitioning(true); } // 拖拽取消时也执行同样的过渡逻辑 onDragCancel={() => setIsTransitioning(true)}
4. 重构OverlayItem组件传递Ref
使用forwardRef让OverlayItem能接收并绑定Ref:
const OverlayItem = forwardRef<HTMLDivElement, { shelf: Shelf }>(({ shelf }, ref) => { return ( <div ref={ref} className="flex items-center gap-2 p-3 rounded-md border border-blue-400 bg-blue-50 shadow-lg w-80" > <span className="px-2 py-1 rounded bg-gray-200">☰</span> <span>{shelf.name}</span> </div> ); }); OverlayItem.displayName = "OverlayItem";
5. 更新DragOverlay的使用
将Ref传递给OverlayItem:
<DragOverlay> {activeShelf ? <OverlayItem ref={overlayRef} shelf={activeShelf} /> : null} </DragOverlay>
修改后的完整代码
"use client"; import React, { useState, useRef, useEffect, forwardRef } 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> ); } const OverlayItem = forwardRef<HTMLDivElement, { shelf: Shelf }>(({ shelf }, ref) => { return ( <div ref={ref} className="flex items-center gap-2 p-3 rounded-md border border-blue-400 bg-blue-50 shadow-lg w-80" > <span className="px-2 py-1 rounded bg-gray-200">☰</span> <span>{shelf.name}</span> </div> ); }); OverlayItem.displayName = "OverlayItem"; 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 overlayRef = useRef<HTMLDivElement>(null); const sensors = useSensors(useSensor(PointerSensor)); useEffect(() => { const handleTransitionEnd = () => { if (isTransitioning) { setActiveId(null); setIsTransitioning(false); } }; const element = overlayRef.current; if (element) { element.addEventListener('transitionend', handleTransitionEnd); } return () => { if (element) { element.removeEventListener('transitionend', handleTransitionEnd); } }; }, [isTransitioning]); 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={() => setIsTransitioning(true)} > <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 ref={overlayRef} shelf={activeShelf} /> : null} </DragOverlay> </DndContext> </div> ); }
效果说明
修改后,拖拽释放的流程会变成:
- 拖拽元素 → 列表中出现Placeholder
- 释放元素 → DragOverlay开始执行归位动画,Placeholder保持可见
- 动画完全结束 → Placeholder消失,UI过渡自然无跳转
内容的提问来源于stack exchange,提问作者user32470124
相关产品推荐
相关产品推荐

