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,别的作者也推荐使用这个。

相关推荐
有什么东东1 小时前
力扣练习之确定两个字符串是否接近
前端·算法·leetcode
鱼樱前端1 小时前
全前端需要的工程化能力之 Vue3 + TypeScript + Vite 工程化项目搭建最佳实践
前端·vue.js
明远湖之鱼1 小时前
手把手带你实现 Vite+React 的简易 SSR 改造【含部分原理讲解】
前端·react.js·vite
野生的程序媛2 小时前
重生之我在学Vue--第10天 Vue 3 项目收尾与部署
前端·javascript·vue.js
烟锁池塘柳02 小时前
技术栈的概念及其组成部分的介绍
前端·后端·web
加减法原则2 小时前
面试题之虚拟DOM
前端
故事与他6453 小时前
Tomato靶机攻略
android·linux·服务器·前端·网络·web安全·postcss
jtymyxmz3 小时前
mac 苍穹外卖 前端环境配置
前端
烛阴3 小时前
JavaScript Rest 参数:新手也能轻松掌握的进阶技巧!
前端·javascript
chenchihwen3 小时前
ITSM统计分析:提升IT服务管理效能 实施步骤与操作说明
java·前端·数据库