React DnD API详解与应用案例 – wiki基地

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 用于将组件定义为拖动源。它接受一个配置对象作为参数,并返回一个包含三个元素的数组:

  1. 收集函数返回的 props:你可以将这些 props 应用到你的组件上。
  2. 拖动源的引用 (drag ref):你需要将这个引用附加到你想要使其可拖动的 DOM 节点上。
  3. 拖动预览的引用 (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 (

{name}

);
}
“`

2.2 useDrop Hook

useDrop 用于将组件定义为放置目标。它接受一个配置对象作为参数,并返回一个包含两个元素的数组:

  1. 收集函数返回的 props:你可以将这些 props 应用到你的组件上。
  2. 放置目标的引用 (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 (

{/ 根据 itemType 和 item 渲染自定义的拖动预览 /}

);
}
“`

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 构建一个可拖拽的卡片列表。

目标:

  • 创建一个包含多个卡片的列表。
  • 允许用户通过拖放来重新排序卡片。
  • 在拖动时显示一个半透明的预览。

步骤:

  1. 创建 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}

);
}
“`

  1. 创建 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) => (

))}

);
}
“`

  1. 使用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,并在你的项目中发挥它的作用。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部