React DnD API 详解与应用案例
React DnD 是一个强大的 React 库,用于构建复杂的拖放界面。它基于 HTML5 的拖放 API,但提供了一个更声明式、更易于使用的抽象层,让你能够轻松地将拖放功能集成到 React 应用中。本文将深入探讨 React DnD 的核心概念、API 以及实际应用案例,帮助你全面掌握这个强大的工具。
1. React DnD 核心概念
在深入了解 API 之前,我们需要先理解 React DnD 的几个核心概念:
- 拖动源 (Drag Source):表示可以被拖动的组件。你需要指定该组件的类型(用于标识拖动项),以及在拖动开始、进行中、结束时如何收集数据和处理副作用。
- 放置目标 (Drop Target):表示可以放置拖动项的组件。你需要指定它可以接受的拖动项类型,以及在拖动项悬停、放置时如何处理数据和更新状态。
- 监视器 (Monitor):一个对象,包含了当前拖放操作的状态信息,例如拖动项的类型、位置、是否在放置目标上方等。你可以通过监视器来更新组件的 UI 或执行其他操作。
- 收集函数 (Collecting Function):一个函数,用于从监视器中提取你需要的数据,并将其作为 props 传递给你的组件。
- 后端 (Backend):React DnD 抽象了底层的拖放实现,通过后端来处理与具体平台的交互。最常用的后端是 HTML5 Backend,它利用了浏览器的原生拖放 API。你也可以使用其他的后端,例如 Touch Backend 来支持触摸设备。
2. React DnD API 详解
React DnD 提供了几个主要的 Hook 和高阶组件(HOC)来实现拖放功能。我们将详细介绍这些 API 的用法。
2.1 useDrag
Hook
useDrag
用于将组件定义为拖动源。它接受一个配置对象作为参数,并返回一个包含三个元素的数组:
- 收集函数返回的 props:你可以将这些 props 应用到你的组件上。
- 拖动源的引用 (drag ref):你需要将这个引用附加到你想要使其可拖动的 DOM 节点上。
- 拖动预览的引用 (drag preview ref):你可以将这个引用附加到自定义的拖动预览 DOM 节点上,如果不指定,React DnD 会使用被拖动元素的克隆作为预览。
useDrag
的配置对象包含以下属性:
type
(必需):一个字符串或 symbol,表示拖动项的类型。只有相同类型的拖动源和放置目标才能进行交互。item
(可选):一个对象或一个返回对象的函数,表示被拖动的数据。这个数据会在放置时传递给放置目标。collect
(可选):一个收集函数,用于从监视器中提取数据并作为 props 传递给组件。canDrag
(可选):一个布尔值或一个返回布尔值的函数,用于确定当前是否允许拖动。isDragging
(可选):一个返回布尔值的函数,用于自定义拖动状态的判断逻辑。end
(可选):一个函数,在拖动结束时调用。你可以在这里处理副作用,例如更新数据或发送请求。previewOptions
: (可选): 一个带有captureDraggingState
布尔属性的对象, 开启该属性,可以捕获拖拽节点DOM变化状态
示例:
“`javascript
import { useDrag } from ‘react-dnd’;
function DraggableItem({ id, name }) {
const [{ isDragging }, drag] = useDrag(() => ({
type: ‘ITEM’,
item: { id, name },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}));
return (
);
}
“`
2.2 useDrop
Hook
useDrop
用于将组件定义为放置目标。它接受一个配置对象作为参数,并返回一个包含两个元素的数组:
- 收集函数返回的 props:你可以将这些 props 应用到你的组件上。
- 放置目标的引用 (drop ref):你需要将这个引用附加到你想要使其可放置的 DOM 节点上。
useDrop
的配置对象包含以下属性:
accept
(必需):一个字符串、symbol 或一个数组,表示可以接受的拖动项类型。collect
(可选):一个收集函数,用于从监视器中提取数据并作为 props 传递给组件。canDrop
(可选):一个布尔值或一个返回布尔值的函数,用于确定当前是否可以放置。hover
(可选):一个函数,在拖动项悬停在放置目标上方时调用。你可以在这里更新组件状态或执行其他操作。drop
(可选):一个函数,在拖动项放置到目标上时调用。你可以在这里处理数据更新、发送请求等。
示例:
“`javascript
import { useDrop } from ‘react-dnd’;
function DropTarget({ onDrop }) {
const [{ canDrop, isOver }, drop] = useDrop(() => ({
accept: ‘ITEM’,
drop: (item, monitor) => onDrop(item),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}));
return (
{canDrop ? ‘Release to drop’ : ‘Drag an item here’}
);
}
“`
2.3 useDragLayer
Hook
useDragLayer
用于创建一个自定义的拖动层。它可以让你完全控制拖动预览的外观和行为。它接受一个收集函数作为参数,并返回收集函数返回的 props。
示例:
“`javascript
import { useDragLayer } from ‘react-dnd’;
function CustomDragLayer() {
const { itemType, isDragging, item, currentOffset } = useDragLayer(
(monitor) => ({
itemType: monitor.getItemType(),
isDragging: monitor.isDragging(),
item: monitor.getItem(),
currentOffset: monitor.getSourceClientOffset(),
})
);
if (!isDragging) {
return null;
}
return (
);
}
“`
2.4 DndProvider
组件
DndProvider
是一个顶层组件,用于为你的应用提供 React DnD 的上下文。你需要将你的整个应用包裹在 DndProvider
中,并指定一个后端。
示例:
“`javascript
import { DndProvider } from ‘react-dnd’;
import { HTML5Backend } from ‘react-dnd-html5-backend’;
function App() {
return (
{/ 你的应用组件 /}
);
}
“`
3. 应用案例:实现一个可拖拽的卡片列表
现在,让我们通过一个实际的案例来演示如何使用 React DnD 构建一个可拖拽的卡片列表。
目标:
- 创建一个包含多个卡片的列表。
- 允许用户通过拖放来重新排序卡片。
- 在拖动时显示一个半透明的预览。
步骤:
- 创建
Card
组件:
“`javascript
import { useDrag, useDrop } from ‘react-dnd’;
import { useRef } from ‘react’;
function 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(),
}),
}));
drag(drop(ref));
return (
{text}
);
}
“`
- 创建
List
组件:
“`javascript
import { useState } from ‘react’;
import { DndProvider } from ‘react-dnd’;
import { HTML5Backend } from ‘react-dnd-html5-backend’;
import Card from ‘./Card’;
function List() {
const [cards, setCards] = useState([
{ id: 1, text: ‘Card 1’ },
{ id: 2, text: ‘Card 2’ },
{ id: 3, text: ‘Card 3’ },
]);
const moveCard = (dragIndex, hoverIndex) => {
const dragCard = cards[dragIndex]
setCards(update(cards, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard],
],
}))
}
return (
{cards.map((card, index) => (
))}
);
}
“`
- 使用immer 或者 @dnd-kit/sortable 辅助更新状态
为了简化状态更新,你可以使用immer
库,代码如下
“`javascript
import { useImmer } from ‘use-immer’;
const moveCard = (dragIndex, hoverIndex) => {
setCards((draft) => {
const dragCard = draft[dragIndex];
draft.splice(dragIndex, 1);
draft.splice(hoverIndex, 0, dragCard);
});
}
或者
javascript
import {
closestCenter,
DndContext,
PointerSensor,
useSensor,
useSensors,
} from “@dnd-kit/core”;
import {
arrayMove,
SortableContext,
verticalListSortingStrategy,
} from “@dnd-kit/sortable”;
function List() {
const [cards, setCards] = useState([
{ id: 1, text: ‘Card 1’ },
{ id: 2, text: ‘Card 2’ },
{ id: 3, text: ‘Card 3’ },
]);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
})
);
const handleDragEnd = (event) => {
const {active, over} = event;
if (active.id !== over.id) {
setCards((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
}
return(
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={cards}
strategy={verticalListSortingStrategy}
>
{cards.map((card, index) => (
<Card
key={card.id}
index={index}
id={card.id}
text={card.text}
moveCard={moveCard}
/>
))}
</SortableContext>
</DndContext>
)
}
“`
解释:
- 我们使用
useState
来管理卡片列表的状态。 moveCard
函数负责更新卡片的位置。List
组件渲染多个Card
组件,并为每个卡片传递必要的 props。- 我们使用了
Card
组件的hover
实现拖拽排序功能。 - 我们使用
DndProvider
将整个列表包裹起来,并指定使用HTML5Backend
。
4. 总结
React DnD 是一个功能强大且灵活的库,可以帮助你轻松地在 React 应用中实现各种拖放功能。通过理解其核心概念和 API,你可以构建出复杂的交互界面,提升用户体验。本文提供的案例只是一个入门级的示例,你可以根据自己的需求进行扩展和定制。希望这篇文章能够帮助你更好地掌握 React DnD,并在你的项目中发挥它的作用。