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版本即可):
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,别的作者也推荐使用这个。