当前位置: 首页 > news >正文

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

需求场景:

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

技术调研:

借用antd Table实现:

我的项目里使用了antd,antd表格有一个示例还是挺像的,本来我想用Table实现,它自带拖拽。但后来想了一下,还要改antd的样式,而且布局不灵活。
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版本即可):
npm install @dnd-kit/react
使用官网例子:

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

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:
npm install react-dnd react-dnd-html5-backend
把官网的例子放到项目里:

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

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

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.indexconst hoverIndex = index// Don't replace items with themselvesif (dragIndex === hoverIndex) {return}// Determine rectangle on screenconst hoverBoundingRect = ref.current?.getBoundingClientRect()// Get vertical middleconst hoverMiddleY =(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2// Determine mouse positionconst clientOffset = monitor.getClientOffset()// Get pixels to the topconst 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 downwardsif (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {return}// Dragging upwardsif (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {return}// Time to actually perform the actionmoveCard(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 : 1drag(drop(ref))return (<div ref={ref} style={{ ...style, opacity }} data-handler-id={handlerId}>{text}</div>)
}
新建一个Container.tsx:

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 (<Cardkey={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:

cnpm install immutability-helper

npm immutability-helper

‌immutability-helper‌是一个小型JavaScript库,旨在提供一种方便的方法来无副作用地修改数据,同时保持原始数据源的不变性。它由Kolodny创建,主要功能是创建数据的副本并对其进行修改,而不是直接修改原数据‌主要功能和用法
immutability-helper提供了一系列API来操作不可变数据,包括:$push:将数组中的所有项推送到目标数组中。
$unshift:将数组中的所有项插入到目标数组的开头。
$splice:对目标数组进行多次操作。
$set:用任意值替换目标。
$toggle:切换目标数组中指定下标的布尔值。
$unset:从目标对象中移除指定的键列表。
$merge:将参数对象的键与目标合并。
$apply:将当前值传递给函数进行处理‌

效果图

我的需求实现代码:
新建一个组件 index.tsx:
import React from "react";
import Example from './Container';
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
// 定义一个名为App的函数
const App = () => {// 返回一个div元素,类名为Appreturn (<DndProvider backend={HTML5Backend}><Example /></DndProvider>);
}
export default App
新建一个Card.tsx:
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) returnconst 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 (<divref={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:
// 引入依赖库
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 (<Cardkey={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库,觉得多一个依赖没啥意义,所以我去掉了。

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


http://www.mrgr.cn/news/94258.html

相关文章:

  • Kotlin和Java区别
  • 达梦主备集群部署
  • 阿里云操作系统控制台评测:国产AI+运维 一站式运维管理平台
  • ROS实践(四)机器人SLAM建图(gmapping)
  • 推理框架SGLang安装与调试
  • LVS + Keepalived 高可用集群
  • 《YOLOE: Real-Time Seeing Anything》论文速览翻译,支持文本提示,视觉提示等开放世界检测算法!
  • Java常见的并发设计模式
  • maven wrapper的使用
  • 爬虫中一些有用的用法
  • Qt:绘图API
  • 【Pytorch Transformers Fine-tune】使用BERT进行情感分类任务微调
  • Selenium 自动化测试学习总结
  • 本地Git仓库搭建(DevStar)与Git基本命令
  • MySQL的安装与建表
  • PySide(PyQT)的mouseMoveEvent()和hoverMoveEvent()的区别
  • java中小型公司面试预习资料(四):微服务架构
  • Unity 封装一个依赖于MonoBehaviour的计时器(上) 基本功能
  • Visual Studio 安装及使用教程(Windows)【安装】
  • JavaScript_Day2