React DnD 官方指南:用 useDrag 构建解耦的拖拽交互(docsRoot 门户文档全解析)
【免费下载链接】react-dndDrag and Drop for React项目地址: https://gitcode.com/gh_mirrors/re/react-dnd
本篇技术指南基于当前仓库中 packages/docsite/markdown/docs/docsRoot.md 门户文档展开,全面讲解 React DnD(Drag and Drop for React)的定位、安装方式、Hooks 风格的拖拽用法,以及它解耦组件、拥抱单向数据流、屏蔽浏览器差异、支持自定义后端的核心设计理念。读完本文,你将掌握如何用react-dnd+react-dnd-html5-backend在十分钟内让任意组件变成可拖拽的拖拽源,理解 Drag Source、Drop Target、Monitor、Connector、Backend 五大抽象之间的关系,并了解如何通过模拟后端在 Node 环境中测试拖拽交互。
React DnD 是什么
React DnD 是一组 React 工具库,帮助你构建复杂的拖拽交互界面,同时保持组件之间的解耦(decoupled)。它非常适合 Trello、Storify 这类应用场景:拖拽在应用的不同部分之间传递数据,组件外观与应用状态会随拖拽事件实时变化。
它不是一个开箱即用的"拖拽组件库",而是一套构建拖拽能力的原语(primitives):它不替你画任何 UI,而是把你的组件包装起来,向组件注入 props 和 refs,由你的组件来决定拖拽时"看起来"是什么样。
安装与后端选型
安装react-dnd时通常需要同时安装一个后端(backend):
npm install react-dnd react-dnd-html5-backend其中:
react-dnd是核心库,提供DndProvider、useDrag、useDrop、useDragLayer等 API;react-dnd-html5-backend在底层使用 HTML5 拖拽 API 处理真实的浏览器拖拽事件。
你完全可以换用第三方后端,例如针对触屏设备的react-dnd-touch-backend(当前仓库中的实现位于 packages/backend-touch/src/index.ts,其触屏实现细节见 TouchBackendImpl.ts)。HTML5 后端与 Touch 后端的详细说明分别见 HTML5.md 与 Touch.md。
从源码看"后端"是什么
在 packages/backend-html5/src/index.ts 中可以看到,HTML5 后端本质上是一个BackendFactory:
export const HTML5Backend: BackendFactory = function createBackend( manager: DragDropManager, context?: HTML5BackendContext, options?: HTML5BackendOptions, ): HTML5BackendImpl { return new HTML5BackendImpl(manager, context, options) }而 packages/dnd-core/src/interfaces.ts 定义了后端需要实现的契约:setup()、teardown()、connectDragSource()、connectDragPreview()、connectDropTarget()。后端只做一件事——把原生 DOM 事件翻译成 React DnD 内部可处理的 Redux action,这一点下文会展开。
快速上手:让一个组件变得可拖拽
官方文档用一段简洁示例展示了useDragHook 的用法——把<Card text='Write the docs' />变成可拖拽组件:
import React from 'react' import { useDrag } from 'react-dnd' import { ItemTypes } from './Constants' /** * Your Component */ export default function Card({ isDragging, text }) { const [{ opacity }, dragRef] = useDrag( () => ({ type: ItemTypes.CARD, item: { text }, collect: (monitor) => ({ opacity: monitor.isDragging() ? 0.5 : 1 }) }), [] ) return ( <div ref={dragRef} style={{ opacity }}> {text} </div> ) }这段代码的关键点:
type:拖拽源的类型标识(字符串或 symbol),只有注册了相同类型的 drop target 才会对它做出反应;item:描述被拖拽数据的纯 JavaScript 对象(这里只有{ text }),这是 drop target 能获得的唯一信息来源,所以应当只放最小必要数据;collect(monitor):收集函数,从 monitor 中取出isDragging()状态并合成为组件 props;- 返回值数组:
[0]是收集到的 props,[1]是拖拽源 ref(dragRef),必须挂到可拖拽的 DOM 节点上。
从源码看 useDrag 的实现
在 packages/react-dnd/src/hooks/useDrag/useDrag.ts 中,useDrag的返回结构一目了然:
export function useDrag<DragObject, DropResult, CollectedProps>( specArg, deps?, ): [CollectedProps, ConnectDragSource, ConnectDragPreview] { const spec = useOptionalFactory(specArg, deps) invariant(!(spec as any).begin, 'useDrag::spec.begin was deprecated in v14. Replace spec.begin() with spec.item(). ...') const monitor = useDragSourceMonitor<DragObject, DropResult>() const connector = useDragSourceConnector(spec.options, spec.previewOptions) useRegisteredDragSource(spec, monitor, connector) return [ useCollectedProps(spec.collect, monitor, connector), useConnectDragSource(connector), useConnectDragPreview(connector), ] }值得注意的实现细节:
- spec 可以是对象也可以是工厂函数,
useOptionalFactory会结合deps做记忆化;官方推荐函数形式,因为它能闭包读取最新 props; - v14 起
spec.begin已废弃,必须改用spec.item,源码中的invariant断言会在误用时抛出明确错误(见 useDrag.ts); - 返回的第二个元素是
ConnectDragSource(拖拽源 ref),第三个是ConnectDragPreview(拖拽预览 ref),分别对应后端契约中的connectDragSource与connectDragPreview。
对应地,放置目标的useDrop返回[CollectedProps, ConnectDropTarget],见 packages/react-dnd/src/hooks/useDrop/useDrop.ts。
别忘了 DndProvider
使用这些 Hooks 之前,需要用DndProvider在组件树根部注入 DnD 上下文,例如配合 HTML5 后端:
import { DndProvider } from 'react-dnd' import { HTML5Backend } from 'react-dnd-html5-backend' <DndProvider backend={HTML5Backend}> <Card text='Write the docs' /> </DndProvider>DndProvider的实现见 packages/react-dnd/src/core/DndProvider.tsx:它接收backend工厂函数与可选的options、debugMode,内部调用createDragDropManager创建管理器,再通过DndContext.Provider向下层组件提供;若未传入context,管理器会以全局单例形式缓存(__REACT_DND_CONTEXT_INSTANCE__符号),组件卸载时通过 refCount 清理,避免内存泄漏。
createDragDropManager的底层逻辑在 packages/dnd-core/src/createDragDropManager.ts:它创建一个 Redux store(debugMode为 true 且存在 Redux DevTools 扩展时自动接入,见 createDragDropManager.ts),再构建 Monitor、HandlerRegistry、Manager,最后由backendFactory(manager, globalContext, backendOptions)实例化后端并注入。React DnD 本身就是构建在 Redux 之上的。
四大设计特性(Features)
官方文档总结了 React DnD 的四个核心特性,结合源码可以看得更深:
1. 与你的组件协作,而非提供现成组件
React DnD 不提供 readymade 组件,而是包装你的组件并注入 props——如果你用过 React Router 或 Flummox,对这种模式应该不陌生。useDrag返回的[collectedProps, dragRef, dragPreviewRef]就是这种"注入"的具体形态:collected props 由你决定,refs 由你决定挂到哪里,框架只负责在恰当的时机更新它们。
2. 拥抱单向数据流
React DnD 完全遵循 React 的声明式渲染范式,不直接修改 DOM,与 Redux 以及其它单向数据流架构天然互补——事实上它本身就是构建在 Redux 之上的。
从源码结构可以印证这一点:packages/dnd-core/src/reducers/ 下有一组纯 reducer(dragOffset.ts、dragOperation.ts、dirtyHandlerIds.ts、refCount.ts、stateId.ts),packages/dnd-core/src/actions/dragDrop/ 下是标准的 Redux action creators(beginDrag、hover、drop、endDrag、publishDragSource)。所有的拖拽状态——是否正在拖拽、当前 item、指针偏移量、drop 结果——都保存在这个 Redux store 中,由 reducer 纯函数驱动更新。组件通过 Monitor 订阅这些状态变化并重新收集 props,形成完整的单向数据流闭环。
3. 隐藏平台怪癖
HTML5 拖拽 API 充满坑和浏览器不一致性,React DnD 在内部处理它们,让你专注于业务开发。例如:
- HTML5 后端会自动对拖拽中的 DOM 节点"截图"作为拖拽预览,你不用自己绘制跟随光标的内容;
- 后端抽象了浏览器差异,其角色类似 React 的合成事件系统,但不依赖 React 或 React 的合成事件系统(见 packages/dnd-core/src/interfaces.ts 中后端接口与 React 无任何耦合)。
4. 可扩展、可测试
React DnD 默认使用 HTML5 后端,但允许你注入自定义"后端":可以基于触摸事件、鼠标事件,或者完全自定义。例如内置的模拟后端(react-dnd-test-backend,见 packages/backend-test/src/TestBackend.ts)可以让你在Node 环境中测试组件的拖拽交互——这正是 examples 下大量集成测试(如 single-target-integration.spec.tsx、dragSources.spec.tsx)赖以运行的基础。
核心概念速览(源自 Overview 的延伸)
docsRoot.md是文档门户,其内容与 00 Quick Start/Overview.md 一脉相承。理解以下五个概念,就能看懂整个文档体系:
- Item 与 Type:拖拽的不是 DOM 节点,而是"某种类型的 item"——一个描述被拖拽数据的纯对象(如
{ cardId: 42 })。Type 是字符串或 symbol,用于声明拖拽源与放置目标的兼容性; - Monitor:拖拽是天然有状态的,Monitor 是对内部状态存储的轻量封装,让你能响应状态变化更新组件 props;
- Connector:把"拖拽源 / 拖拽预览 / 放置目标"三种角色赋予具体 DOM 节点的函数,内部通过 callback ref 附加事件,且返回的函数是记忆化的,不会破坏
shouldComponentUpdate优化; - Drag Source 与 Drop Target:把类型、item、副作用和收集函数与组件绑定起来的主要抽象单元;drop target 可以同时注册多个类型;
- Backend:把 DOM 事件翻译为内部 Redux action 的可插拔实现。
现代推荐用 Hooks API(useDrag、useDrop、useDragLayer、useDragDropManager),其完整讲解见 HooksOverview.md、useDrag.md 与 useDrop.md;Hooks 的完整导出见 packages/react-dnd/src/hooks/index.ts。
触屏支持(Touch Support)
HTML5 拖拽 API 在触屏设备上不工作。如需触屏支持,请将react-dnd-html5-backend换成 touch backend,即使用 packages/backend-touch:
npm install react-dnd react-dnd-touch-backendimport { DndProvider } from 'react-dnd' import { TouchBackend } from 'react-dnd-touch-backend' <DndProvider backend={TouchBackend} options={{ enableMouseEvents: true }}> ... </DndProvider>触屏后端的配置选项(如enableMouseEvents、delay、delayTouchStart等)在 packages/backend-touch/src/interfaces.ts 中定义,并有对应的单元测试覆盖(见 TouchBackend.spec.ts 与 OptionsReader.spec.ts)。
Non-Goals:明确不做的事
React DnD 提供一组强大的原语,但不包含任何现成组件。它比 jQuery UI 或 interact.js 更底层,聚焦于"把拖拽交互做对",而把视觉呈现(如轴向约束、吸附网格)留给你自己实现。
一个典型的例子:React DnD不计划提供Sortable组件,而是给你构建自己 Sortable 所需的工具——排序场景的完整可运行示例见 examples/src/04-sortable/,其中 simple 与 stress-test 展示了如何基于useDrag+useDrop组合出排序能力;拖拽吸附网格的工具函数示例见 snapToGrid.ts。
支持、致谢与许可
- 支持与贡献:Issues 与改进建议的讨论渠道见仓库的 CODEOWNERS、CONTRIBUTING.md 与 CODE_OF_CONDUCT.md;完整变更历史记录在 CHANGELOG.md;
- 致谢:官方感谢 BrowserStack 为维护者提供浏览器问题调试服务;
- 许可:React DnD 以MIT协议开源(见 LICENSE),可自由使用。
下一步阅读路径
门户文档为读者规划了清晰的进阶路线:
- 先读 00 Quick Start/Overview.md,建立 Item/Type、Monitor、Connector、Source/Target、Backend 五大概念;
- 再读 03 Using Hooks/HooksOverview.md 熟悉 Hooks API 全貌;
- 动手实现 00 Quick Start/Tutorial.md 中的国际象棋教程(配套完整示例在 examples/src/00-chessboard/);
- 按需查阅 useDrag.md、useDrop.md、useDragLayer.md 的规范细节,以及 DndProvider.md 的上下文配置说明;
- 后端选型参考 05 Backends/HTML5.md 与 05 Backends/Touch.md;
- 测试方法参考 00 Quick Start/Testing.md,配合
react-dnd-test-backend在 Node 中完成拖拽交互测试。
【免费下载链接】react-dndDrag and Drop for React项目地址: https://gitcode.com/gh_mirrors/re/react-dnd
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考