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

DndKit拖拽排序过渡异常求助:元素复位后再执行过渡

DndKit useSortable 拖拽过渡异常修复方案

问题现象

使用@dnd-kit/sortable的useSortable实现拖拽排序时,释放元素后会先跳回原始位置,再执行过渡动画,存在明显延迟。移除transition属性后功能正常,但需要保留平滑过渡效果。

核心问题分析

  1. 列表项key使用索引导致DOM复用错误:原代码中用数组索引i作为SortableItem的key,拖拽后数组重排时,React会错误复用DOM节点,导致元素位置计算混乱,引发过渡跳变。
  2. transition属性在拖拽状态下未禁用:拖拽过程中应用transition会干扰拖拽结束时的位置重置逻辑,导致过渡动画延迟。

修复步骤

  • 替换列表项key为唯一ID:用service.id替代索引作为key,确保React能正确识别每个列表项的唯一性,避免DOM复用错误。
  • 条件应用transition:仅当元素非拖拽状态时应用transition,拖拽过程中禁用过渡,消除拖拽结束时的冲突。

修改后的代码

SortableItem 组件

import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import React from 'react';
import Icon from '../../../../../../../common/components/icon/Icon';

interface Props {
   id: number;
   children: React.ReactNode;
}

const SortableItem = ({ id, children }: Props) => {
   const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });

   return (
      <div
         ref={setNodeRef}
         style={{
            transform: CSS.Transform.toString(transform),
            // 拖拽时禁用过渡,避免释放时的位置跳变
            transition: isDragging ? undefined : transition,
         }}>
         <div className='grid grid-cols-[60px_auto] items-center w-full h-full relative'>
            <div
               className='flex items-center w-full h-full cursor-grab'
               data-grab={true}
               {...attributes}
               {...listeners}>
               <Icon name='ArrowsUpDownLeftRight' width={20} height={20} />
            </div>
            {children}
         </div>
      </div>
   );
};

export default SortableItem;

EditServices 组件

'use client';
import React from 'react';
import { ListenService } from '../SmartLinkClient';
import Icon from '../../../../../../common/components/icon/Icon';
import Input from '../../../../../../common/components/input/Input';
import ToggleSwitch from '../../../../../../common/components/toggle-switch/ToggleSwitch';
import Hr from '../../../../../../common/components/hr/Hr';
import { DndContext, PointerSensor, useSensor, useSensors, closestCenter } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import SortableItem from './(edit-services-partials)/SortableItem';

interface Props {
   servicesConfig: ListenService[];
   handleServiceShowToggle: (e: any, elementToChange: ListenService) => void;
   handleServiceDragEnd: (active: any, over: any) => void;
}

const EditServices = ({ servicesConfig, handleServiceShowToggle, handleServiceDragEnd }: Props) => {
   const sensors = useSensors(useSensor(PointerSensor));

   return (
      <div className='select-services'>
         <div className='flex flex-col gap-y-4'>
            <DndContext
               sensors={sensors}
               collisionDetection={closestCenter}
               onDragEnd={({ active, over }) => {
                  handleServiceDragEnd(active, over);
               }}>
               <SortableContext
                  items={servicesConfig.map(service => service.id)}
                  strategy={verticalListSortingStrategy}>
                  {/* 使用service.id作为key,避免索引key导致的DOM复用问题 */}
                  {servicesConfig.map((service, i) => (
                     <SortableItem key={service.id} id={service.id}>
                        <div className='grid grid-cols-[minmax(160px,180px)_minmax(200px,auto)_100px] items-center'>
                           <div className='grid grid-cols-[24px_auto] items-center gap-x-3 dark:text-stone-100 text-stone-800'>
                              <Icon name={service.iconName} width={24} height={24} color={service.color} />
                              <span className='text-[16px]'>{service.name}</span>
                           </div>
                           <Input
                              placeholder='We could not find a valid URL, but you can enter your own.'
                              type={'url'}
                           />
                           <div className='justify-self-end -mt-2 flex flex-row items-center gap-x-2'>
                              <span className='text-[11px] text-stone-600 dark:text-stone-300'>Show</span>
                              <ToggleSwitch
                                 toggled={service.show}
                                 handleToggled={(e) => {
                                    handleServiceShowToggle(e, service);
                                 }}
                              />
                           </div>
                        </div>
                        {i !== servicesConfig.length - 1 && (
                           <span className='col-span-2'>
                              <Hr />
                           </span>
                        )}
                     </SortableItem>
                  ))}
               </SortableContext>
            </DndContext>
         </div>
      </div>
   );
};

export default EditServices;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 20:15:55