react实现一个列表的拖拽排序(react实现拖拽)

需求场景:

我的项目里需要实现一个垂直列表的拖拽排序,效果图如下图:

技术调研:

借用antd Table实现:

我的项目里使用了antd,antd表格有一个示例还是挺像的,本来我想用Table实现,它自带拖拽。但后来想了一下,还要改antd的样式,而且布局不灵活。

antd Table 拖拽排序

库调研:

核心库分类与特点

react-dnd‌

‌核心能力‌:基于 HTML5 拖拽 API 设计,支持复杂场景(跨容器、嵌套拖拽)‌

‌优势‌:灵活度高,可搭配 HTML5Backend 或自定义后端实现多端兼容‌

‌缺点‌:需手动处理动画效果,开发成本较高‌
github react-dnd

请参阅网站上的文档、教程和示例(这个需要用梯子才能访问):

http://react-dnd.github.io/react-dnd/

‌react-beautiful-dnd‌

‌核心能力‌:专注列表拖拽排序,内置流畅动画和视觉反馈‌

‌优势‌:API 简洁,适合垂直/水平列表场景‌

‌缺点‌:维护停滞(2025年已不推荐新项目使用)

github react-dnd

请参阅网站上的文档、教程和示例(这个需要用梯子才能访问):

http://react-dnd.github.io/react-dnd/

适合复杂拖拽场景

这个库的相同作者:pragmatic-drag-and-drop (这个库更强大灵活)
react-beautiful-dnd issues 2672

这个作者最后推荐了dnd-kit所以我最后选择了这个库,但其实我这个需求用react-beautiful-dnd‌ 也能实现。

react-beautiful-dnd 例子地址

‌dnd-kit‌

‌核心能力‌:模块化设计,支持拖拽、排序、缩放等多种交互‌

‌优势‌:性能优异(基于 CSS Transform),支持触控设备‌

‌适用场景‌:现代 React 项目优先选择(活跃维护)‌

轻量级(核心包仅 4KB)

提供完整的 ‌拖拽动画/碰撞检测‌

官方文档:dndkit.com

‌react-sortable-hoc‌

‌核心能力‌:通过高阶组件快速实现拖拽排序‌

‌缺点‌:已停止维护,仅适合老旧项目兼容‌

‌react-grid-layout‌

‌核心能力‌:网格布局拖拽(如仪表盘、表单设计器)‌

‌优势‌:内置响应式布局算法,支持拖拽+缩放‌

比较流行的有react-beautiful-dnd和dnd-kit,可能还有react-sortable-hoc,不过这个好像已经不再维护了。现在应该推荐使用比较新的库,比如dnd-kit,因为它更轻量且维护活跃。

选型建议(2025年)

需求场景 推荐库 关键理由
复杂交互(跨容器) react-dnd 灵活性高,支持自定义后端
列表排序+动画 dnd-kit 性能优,维护活跃,API 友好
网格布局拖拽 react-grid-layout 专为网格设计,支持响应式
老旧项目维护 react-sortable-hoc 快速适配旧代码,无需重构

使用dnd-kit的步骤与代码:

官网文档使用即了解:

例子demo:
dnd-kit 垂直拖拽例子
dnd-kit 垂直拖拽例子 带手柄

切换到 Docs 然后 右下角有个showCode就能看到代码了:

其实这个demo就大致符合我的需求,我只需要修改一下布局即可!

实现效果步骤:

安装dnd-kit (使用 react版本即可):
bash 复制代码
npm install @dnd-kit/react
使用官网例子:

官网例子,点击Docs就能查看一些示例的基础代码:

javascript 复制代码
import { useSortable } from '@dnd-kit/react/sortable';

function Sortable({ id, index }) {
  const { ref } = useSortable({ id, index });

  return (
    <li ref={ref} className="item">Item {id}</li>
  );
}

function App() {
  const items = [1, 2, 3, 4];

  return (
    <ul className="list">
      {items.map((id, index) =>
        <Sortable key={id} id={id} index={index} />
      )}
    </ul>
  );
}
export default App;

如果能拖动说明引入成功了!只需要修改一下布局即可。
dnd-kit react 官网档
dnd-kit react官网例子

使用react-dnd的步骤与代码:

官网文档使用即了解:

官网的例子代码:github react-dnd examples示例代码

例子网址https://react-dnd.github.io/react-dnd/examples/sortable/simple

这个的代码地址:sortable simple

react-dnd-main\packages\examples\src\04-sortable\simple:

index.ts没什么用可以不用看。

建议之际gitcloe 下来或者 下载成zip在本地打开更方便!

具体实现react-dnd:

下载react-dnd:
bash 复制代码
npm install react-dnd react-dnd-html5-backend
把官网的例子放到项目里:

把官网的例子放到代码里看看能不能运行,能的话说明依赖下载成功。
react-dnd 推拽排序例子

新建一个组件 index.tsx:
javascript 复制代码
import React from "react";
import Example from './Container';
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
// 定义一个名为App的函数
const App = () => {
  // 返回一个div元素,类名为App
  return (
    <DndProvider backend={HTML5Backend}>
      <Example />
    </DndProvider>

  );
}
export default App
新建一个Card.tsx:
javascript 复制代码
import React, { useRef } from 'react';
import { useDrag, useDrop } from 'react-dnd'
// import { ItemTypes } from './ItemTypes.js'
const style = {
  border: '1px dashed gray',
  padding: '0.5rem 1rem',
  marginBottom: '.5rem',
  backgroundColor: 'white',
  cursor: 'move',
}
export const Card = ({ id, text, index, moveCard }) => {
  const ref = useRef(null)
  const [{ handlerId }, drop] = useDrop({
    accept: "card",
    collect(monitor) {
      return {
        handlerId: monitor.getHandlerId(),
      }
    },
    hover(item, monitor) {
      if (!ref.current) {
        return
      }
      const dragIndex = item.index
      const hoverIndex = index
      // Don't replace items with themselves
      if (dragIndex === hoverIndex) {
        return
      }
      // Determine rectangle on screen
      const hoverBoundingRect = ref.current?.getBoundingClientRect()
      // Get vertical middle
      const hoverMiddleY =
        (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
      // Determine mouse position
      const clientOffset = monitor.getClientOffset()
      // Get pixels to the top
      const hoverClientY = clientOffset.y - hoverBoundingRect.top
      // Only perform the move when the mouse has crossed half of the items height
      // When dragging downwards, only move when the cursor is below 50%
      // When dragging upwards, only move when the cursor is above 50%
      // Dragging downwards
      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
        return
      }
      // Dragging upwards
      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
        return
      }
      // Time to actually perform the action
      moveCard(dragIndex, hoverIndex)
      // Note: we're mutating the monitor item here!
      // Generally it's better to avoid mutations,
      // but it's good here for the sake of performance
      // to avoid expensive index searches.
      item.index = hoverIndex
    },
  })
  const [{ isDragging }, drag] = useDrag({
    type: "card",
    item: () => {
      return { id, index }
    },
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
  })
  const opacity = isDragging ? 0 : 1
  drag(drop(ref))
  return (
    <div ref={ref} style={{ ...style, opacity }} data-handler-id={handlerId}>
      {text}
    </div>
  )
}
新建一个Container.tsx:
javascript 复制代码
import update from 'immutability-helper'
import React,{ useCallback, useState } from 'react'
import { Card } from './Card.js'
const style = {
  width: 400,
}
const Container = () => {
  {
    const [cards, setCards] = useState([
      {
        id: 1,
        text: 'Write a cool JS library',
      },
      {
        id: 2,
        text: 'Make it generic enough',
      },
      {
        id: 3,
        text: 'Write README',
      },
      {
        id: 4,
        text: 'Create some examples',
      },
      {
        id: 5,
        text: 'Spam in Twitter and IRC to promote it (note that this element is taller than the others)',
      },
      {
        id: 6,
        text: '???',
      },
      {
        id: 7,
        text: 'PROFIT',
      },
    ])
    const moveCard = useCallback((dragIndex, hoverIndex) => {
      setCards((prevCards) =>
        update(prevCards, {
          $splice: [
            [dragIndex, 1],
            [hoverIndex, 0, prevCards[dragIndex]],
          ],
        }),
      )
    }, [])
    const renderCard = useCallback((card, index) => {
      return (
        <Card
          key={card.id}
          index={index}
          id={card.id}
          text={card.text}
          moveCard={moveCard}
        />
      )
    }, [])
    return (
      <>
        <div style={style}>{cards.map((card, i) => renderCard(card, i))}</div>
      </>
    )
  }
}
export default Container

如果你使用的是jsx后缀改为jsx即可,这个例子需要单独下载一个immutability-helper:

bash 复制代码
cnpm install immutability-helper

npm immutability-helper

bash 复制代码
‌immutability-helper‌是一个小型JavaScript库,旨在提供一种方便的方法来无副作用地修改数据,同时保持原始数据源的不变性。它由Kolodny创建,主要功能是创建数据的副本并对其进行修改,而不是直接修改原数据‌

主要功能和用法
immutability-helper提供了一系列API来操作不可变数据,包括:

$push:将数组中的所有项推送到目标数组中。
$unshift:将数组中的所有项插入到目标数组的开头。
$splice:对目标数组进行多次操作。
$set:用任意值替换目标。
$toggle:切换目标数组中指定下标的布尔值。
$unset:从目标对象中移除指定的键列表。
$merge:将参数对象的键与目标合并。
$apply:将当前值传递给函数进行处理‌
我的需求实现代码:
新建一个组件 index.tsx:
javascript 复制代码
import React from "react";
import Example from './Container';
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
// 定义一个名为App的函数
const App = () => {
  // 返回一个div元素,类名为App
  return (
    <DndProvider backend={HTML5Backend}>
      <Example />
    </DndProvider>

  );
}
export default App
新建一个Card.tsx:
javascript 复制代码
import React, { useRef } from 'react';
import { useDrag, useDrop } from 'react-dnd'  // 拖拽库核心API
import "./index.less"
import moveIcon from "@/assets/img/icon/move.png";  // 拖拽手柄图标
import closeIcon from "@/assets/img/icon/close.png";  // 关闭按钮图标

/**
 * 可拖拽排序卡片组件
 * @param {number} id - 卡片唯一标识
 * @param {string} text - 卡片显示文本
 * @param {number} index - 卡片在列表中的位置索引
 * @param {function} moveCard - 卡片位置交换回调
 * @param {function} closeCard - 卡片关闭回调
 */
export const Card = ({ id, text, index, moveCard, closeCard }) => {
  // 引用DOM节点用于拖拽定位
  const ref = useRef(null)

  /* 放置区域配置 */
  const [{ handlerId }, drop] = useDrop({
    accept: "card",  // 只接受同类型拖拽元素
    collect: (monitor) => ({
      // 获取拖拽源的处理器ID(用于调试)
      handlerId: monitor.getHandlerId(),
    }),
    // 悬停时触发排序逻辑
    hover(item, monitor) {
      if (!ref.current) return
      
      const dragIndex = item.index    // 拖拽元素的原始索引
      const hoverIndex = index        // 当前卡片的索引
      
      // 相同位置不处理
      if (dragIndex === hoverIndex) return

      // 获取当前卡片布局信息
      const hoverBoundingRect = ref.current.getBoundingClientRect()
      // 计算卡片垂直中点
      const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
      // 获取鼠标当前位置
      const clientOffset = monitor.getClientOffset()
      // 计算鼠标相对于卡片顶部的位置
      const hoverClientY = clientOffset.y - hoverBoundingRect.top

      /* 拖拽方向判断逻辑 */
      // 向下拖拽:仅当鼠标超过50%高度时触发交换
      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return
      // 向上拖拽:仅当鼠标超过50%高度时触发交换
      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return

      // 执行卡片位置交换
      moveCard(dragIndex, hoverIndex)
      // 性能优化:直接修改监控项索引避免重复计算
      item.index = hoverIndex
    }
  })

  /* 拖拽行为配置 */
  const [{ isDragging }, drag] = useDrag({
    type: "card",  // 拖拽类型标识
    item: () => ({
      // 传递拖拽所需数据
      id,
      index
    }),
    collect: (monitor) => ({
      // 收集拖拽状态
      isDragging: monitor.isDragging()
    })
  })

  // 将拖拽和放置逻辑绑定到同一DOM节点
  drag(drop(ref))

  return (
    <div
      ref={ref}
      className={isDragging ? "move list_item" : "no_move list_item"}  // 拖拽时应用特殊样式
      data-handler-id={handlerId}  // 调试用标识
    >
      {/* 拖拽手柄 */}
      <img src={moveIcon} alt="move_icon" className='move_icon' />
      
      {/* 卡片内容 */}
      <div className='list_item_content'>
        {text}
      </div>

      {/* 关闭按钮 */}
      <img 
        src={closeIcon} 
        alt="close_icon" 
        className='close_icon' 
        onClick={() => closeCard(id)}  // 传递当前卡片ID
      />
    </div>
  )
}
新建一个Container.tsx:
javascript 复制代码
// 引入依赖库
import update from 'immutability-helper'  // 不可变数据操作工具
import React, { useCallback, useEffect, useState } from 'react'
import { Card } from './Card.js'  // 自定义卡片组件

// 容器样式
const style = {
  width: 400,
}

const Container = () => {
  // 卡片初始状态
  const [cards, setCards] = useState([
    { id: 1, text: '策略类型' },
    { id: 2, text: '策略场景' },
    { id: 3, text: '上周' },
    { id: 4, text: '近一月' },
    { id: 5, text: '今年以来' }
  ])

  // 卡片副本状态(当前未实际使用)
  const [cardCopy, setCardCopy] = useState(cards)

  // 同步主状态到副本状态
  useEffect(() => {
    console.log(cards, "cardCopy")
    setCardCopy(cards)
  }, [cards])

  /**
   * 卡片移动逻辑
   * @param dragIndex 拖拽卡片的原始位置
   * @param hoverIndex 拖拽目标位置
   */
  const moveCard = useCallback((dragIndex, hoverIndex) => {
    setCards(prevCards => 
      update(prevCards, {
        $splice: [
          [dragIndex, 1],          // 删除原始位置的元素
          [hoverIndex, 0, prevCards[dragIndex]]  // 在目标位置插入拖拽元素
        ],
      })
    )
  }, [])

  // const moveCard = useCallback((dragIndex, hoverIndex) => {
  //   setCards(prevCards => {
  //     // 创建新数组副本
  //     const newCards = [...prevCards]
  //     // 提取被拖拽元素
  //     const draggedCard = newCards[dragIndex]
  //     // 删除原位置元素
  //     newCards.splice(dragIndex, 1)
  //     // 插入到新位置
  //     newCards.splice(hoverIndex, 0, draggedCard)
  //     return newCards
  //   })
  // }, [])

  /**
   * 关闭卡片逻辑(当前实现存在问题)
   * @param id 卡片ID
   * @param index 卡片索引
   */
  const closeCard = useCallback((id) => {
    setCards(prevCards =>
      // 使用 filter 创建新数组,排除目标卡片
      prevCards.filter(card => card.id !== id)
    )
  }, [])

  // 渲染单个卡片
  const renderCard = useCallback((card, index) => {
    return (
      <Card
        key={card.id}
        index={index}
        id={card.id}
        text={card.text}
        moveCard={moveCard}
        closeCard={closeCard}
      />
    )
  }, [])

  return (
    <>
      {/* 卡片容器 */}
      <div style={style}>
        {cards.map((card, i) => renderCard(card, i))}
      </div>
    </>
  )
}

export default Container

这里有两个版本,我不想用immutability-helper库,觉得多一个依赖没啥意义,所以我去掉了。

javascript 复制代码
import React, { useCallback, useEffect, useState } from 'react'
import { Card } from './Card.js'  // 自定义卡片组件

// 容器样式
const style = {
  width: 400,
}

const Container = () => {
  // 卡片初始状态
  const [cards, setCards] = useState([
    { id: 1, text: '策略类型' },
    { id: 2, text: '策略场景' },
    { id: 3, text: '上周' },
    { id: 4, text: '近一月' },
    { id: 5, text: '今年以来' }
  ])

  // 卡片副本状态(当前未实际使用)
  const [cardCopy, setCardCopy] = useState(cards)

  // 同步主状态到副本状态
  useEffect(() => {
    console.log(cards, "cardCopy")
    setCardCopy(cards)
  }, [cards])


  /**
     * 卡片移动逻辑
     * @param dragIndex 拖拽卡片的原始位置
     * @param hoverIndex 拖拽目标位置
  */
  const moveCard = useCallback((dragIndex, hoverIndex) => {
    setCards(prevCards => {
      // 创建新数组副本
      const newCards = [...prevCards]
      // 提取被拖拽元素
      const draggedCard = newCards[dragIndex]
      // 删除原位置元素
      newCards.splice(dragIndex, 1)
      // 插入到新位置
      newCards.splice(hoverIndex, 0, draggedCard)
      return newCards
    })
  }, [])

  /**
   * 关闭卡片逻辑(当前实现存在问题)
   * @param id 卡片ID
   * @param index 卡片索引
   */
  const closeCard = useCallback((id) => {
    setCards(prevCards =>
      // 使用 filter 创建新数组,排除目标卡片
      prevCards.filter(card => card.id !== id)
    )
  }, [])

  // 渲染单个卡片
  const renderCard = useCallback((card, index) => {
    return (
      <Card
        key={card.id}
        index={index}
        id={card.id}
        text={card.text}
        moveCard={moveCard}
        closeCard={closeCard}
      />
    )
  }, [])

  return (
    <>
      {/* 卡片容器 */}
      <div style={style}>
        {cards.map((card, i) => renderCard(card, i))}
      </div>
    </>
  )
}

export default Container

总结:

dnd-kit 有react版本也有 所有库都能用的版本:

@dnd-kit/react 就是react版本

@dnd-kit/core就是所有都能用的版本

我这个需求简单,我直接用了react版本,使用很简单!
如果你需求复杂,我还是建议使用 dnd-kit的!!

相对来看 dnd-kit/react相对实现简单,具体用什么你自己来定夺。

react-dnd也还可以,不过我还是倾向于 dnd-kit,别的作者也推荐使用这个。

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax