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

如何实现react-beautiful-dnd拖拽后重渲染组件的过渡动画?

我之前在项目里也碰到过完全一样的问题——react-beautiful-dnd自带的过渡只覆盖拖拽过程,当拖拽结束后元素因为业务逻辑(比如你说的直接跳到目标列表首位)触发重渲染时,确实不会自动带动画。不过有几个经过实践验证的方案可以解决这个问题,分享给你:

方案1:结合状态控制的CSS过渡动画

核心思路是在拖拽结束后,给目标元素添加临时的动画状态,通过CSS过渡实现位置变化的平滑效果,动画结束后再移除状态。

具体步骤:

  1. 给每个可拖拽元素维护一个isAnimating状态,初始为false
  2. 在onDragEnd回调中,判断需要调整到首位的场景,更新列表数据的同时,标记该元素进入动画状态
  3. 利用CSS的transition属性定义过渡效果,通过状态切换触发动画
  4. 监听动画结束事件,移除动画状态,避免影响后续交互

代码示例:

import { useState } from 'react';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';

const ListItem = ({ item, isAnimating, onAnimationEnd }) => {
  return (
    <div
      className={`list-item ${isAnimating ? 'slide-to-top' : ''}`}
      onAnimationEnd={onAnimationEnd}
    >
      {item.content}
    </div>
  );
};

const DragListDemo = () => {
  const [lists, setLists] = useState({
    source: [{ id: '1', content: '源列表项1' }, { id: '2', content: '源列表项2' }],
    target: [{ id: '3', content: '目标列表项1' }, { id: '4', content: '目标列表项2' }]
  });
  const [animatingItemId, setAnimatingItemId] = useState(null);

  const onDragEnd = (result) => {
    if (!result.destination) return;
    const { source, destination, draggableId } = result;

    // 处理跨列表拖拽并插入到目标列表首位的逻辑
    if (source.droppableId !== destination.droppableId) {
      const sourceList = lists[source.droppableId];
      const draggedItem = sourceList.find(item => item.id === draggableId);
      
      // 更新列表数据
      setLists({
        ...lists,
        [source.droppableId]: sourceList.filter(item => item.id !== draggableId),
        [destination.droppableId]: [draggedItem, ...lists[destination.droppableId]]
      });
      
      // 标记需要动画的元素
      setAnimatingItemId(draggableId);
    }
  };

  const handleAnimationEnd = () => {
    setAnimatingItemId(null);
  };

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <div style={{ display: 'flex', gap: '20px', padding: '20px' }}>
        {/* 源列表 */}
        <Droppable droppableId="source">
          {(provided) => (
            <div 
              {...provided.droppableProps} 
              ref={provided.innerRef}
              style={{ width: '200px', border: '1px dashed #ccc', padding: '10px' }}
            >
              {lists.source.map((item, index) => (
                <Draggable key={item.id} draggableId={item.id} index={index}>
                  {(provided) => (
                    <div
                      ref={provided.innerRef}
                      {...provided.draggableProps}
                      {...provided.dragHandleProps}
                    >
                      <ListItem 
                        item={item} 
                        isAnimating={animatingItemId === item.id}
                        onAnimationEnd={handleAnimationEnd}
                      />
                    </div>
                  )}
                </Draggable>
              ))}
              {provided.placeholder}
            </div>
          )}
        </Droppable>

        {/* 目标列表 */}
        <Droppable droppableId="target">
          {(provided) => (
            <div 
              {...provided.droppableProps} 
              ref={provided.innerRef}
              style={{ width: '200px', border: '1px dashed #ccc', padding: '10px' }}
            >
              {lists.target.map((item, index) => (
                <Draggable key={item.id} draggableId={item.id} index={index}>
                  {(provided) => (
                    <div
                      ref={provided.innerRef}
                      {...provided.draggableProps}
                      {...provided.dragHandleProps}
                    >
                      <ListItem 
                        item={item} 
                        isAnimating={animatingItemId === item.id}
                        onAnimationEnd={handleAnimationEnd}
                      />
                    </div>
                  )}
                </Draggable>
              ))}
              {provided.placeholder}
            </div>
          )}
        </Droppable>
      </div>
    </DragDropContext>
  );
};

export default DragListDemo;

对应的CSS:

.list-item {
  padding: 10px;
  margin: 8px 0;
  background: #f5f5f5;
  border-radius: 4px;
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.slide-to-top {
  transform: translateY(-100%);
}
方案2:使用react-transition-group处理列表重排序动画

如果需要更复杂的入场/退场动画,可以结合react-transition-group的TransitionGroup和CSSTransition组件,它能自动检测列表元素的变化并触发动画。

核心要点:

  1. 用TransitionGroup包裹整个可拖拽列表
  2. 每个列表项用CSSTransition包裹,设置唯一的key(必须用元素的唯一ID,不能用索引)
  3. 定义enter、enter-active、exit、exit-active等CSS类来控制动画
关键注意事项
  • 确保每个Draggable组件的key和draggableId都使用元素的唯一ID,不能用索引,否则React无法正确识别元素的位置变化
  • 动画时长要和CSS过渡时长匹配,避免出现状态不同步的问题
  • 如果是横向列表,把translateY换成translateX即可适配方向

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 09:13:06