PAYATHON 2026

dnd-kit 中如何让占位元素等 DragOverlay 动画结束后再消失?

数据阿明

handleDragEnd 会立即调用 setActiveId(null),使 activeShelf 变成 undefined。此时 <DragOverlay> 的子元素会卸载,内部的返回动画无法完整播放。要让占位元素在动画期间继续显示,需要分别处理“拖拽结束”和“清除占位及 overlay 内容”。

保留拖动项目的数据

拖动开始时保存当前项目。拖动结束后先更新排序,暂时保留 activeId,等 DragOverlay 的 drop animation 播放完成后再清除相关状态。

下面以 250ms 为例,并将 DragOverlay 的动画时长设为相同的值。把这些改动合并到组件中:

import React, { useEffect, useRef, useState } from "react";

const DROP_ANIMATION_DURATION = 250;

// Home 组件中:
const [activeId, setActiveId] = useState<number | null>(null);
const [overlayShelf, setOverlayShelf] = useState<Shelf | null>(null);
const clearTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

function clearPendingTimer() {
  if (clearTimer.current !== null) {
    clearTimeout(clearTimer.current);
    clearTimer.current = null;
  }
}

function finishDrag() {
  clearPendingTimer();
  clearTimer.current = setTimeout(() => {
    setActiveId(null);
    setOverlayShelf(null);
    clearTimer.current = null;
  }, DROP_ANIMATION_DURATION);
}

useEffect(() => {
  return () => clearPendingTimer();
}, []);

function handleDragStart(event: any) {
  clearPendingTimer();
  const id = event.active.id as number;
  setActiveId(id);
  setOverlayShelf(shelves.find((shelf) => shelf.id === id) ?? null);
}

function handleDragEnd(event: any) {
  const { active, over } = event;

  if (over && active.id !== over.id) {
    const oldIndex = shelves.findIndex((shelf) => shelf.id === active.id);
    const newIndex = shelves.findIndex((shelf) => shelf.id === over.id);

    if (oldIndex !== -1 && newIndex !== -1) {
      setShelves((items) => arrayMove(items, oldIndex, newIndex));
    }
  }

  finishDrag();
}

function handleDragCancel() {
  finishDrag();
}

然后调整 DndContext 和 DragOverlay:

<DndContext
  sensors={sensors}
  collisionDetection={closestCenter}
  onDragStart={handleDragStart}
  onDragEnd={handleDragEnd}
  onDragCancel={handleDragCancel}
>
  {/* SortableContext 和列表保持原有结构 */}

  <DragOverlay dropAnimation={{ duration: DROP_ANIMATION_DURATION }}>
    {overlayShelf ? <OverlayItem shelf={overlayShelf} /> : null}
  </DragOverlay>
</DndContext>

列表中的占位判断仍使用 isPlaceholder={activeId === shelf.id}。拖动结束后,activeId 会保留一个动画时长,占位元素因此不会立即消失。overlayShelf 也会让 overlay 子元素继续挂载。计时结束后,两者会一起清除。

注意事项

调整 dropAnimation.duration 时,也要同步调整延迟清除的时长。延迟过短可能导致动画闪断,过长则会让占位元素在动画结束后多停留一会儿。拖动取消时也调用相同的清理流程,避免占位元素残留。

备注:内容仅供参考。