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
使用官网例子:
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元素,类名为App
return (
<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.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:
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:
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元素,类名为App
return (
<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) 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:
// 引入依赖库
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库,觉得多一个依赖没啥意义,所以我去掉了。
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就是所有都能用的版本
这个例子就是用dnd-kit/core实现的
我这个需求简单,我直接用了react版本,使用很简单!
对别来看 dnd-kit/react相对实现简单,具体用什么你自己来定夺。