☰
React 拖拽实战指南:beautiful-react-hooks 中 useDrag 的用法、自定义拖拽图像与数据传递
2026/9/25 2:13:32 网站建设 项目流程
  • 前端
  • 开发工具

【免费下载链接】beautiful-react-hooks

🔥 A collection of beautiful and (hopefully) useful React hooks to speed-up your components and hooks development 🔥

项目地址:https://gitcode.com/gh_mirrors/be/beautiful-react-hooks
点击查看免费下载

在 React 项目中实现"可拖拽元素"往往要手写draggable属性、dragstart/dragend监听器、组件卸载时的清理逻辑,以及DataTransfer的数据序列化细节。beautiful-react-hooks提供的useDrag将这一整套流程收敛为一次 Hook 调用:传入一个 DOM ref,即可让元素获得原生拖拽能力,并拿到一个表示"是否正在被拖拽"的布尔状态;再通过dragImage、transfer等选项,可自定义拖拽时显示的图像与传递给放置目标的数据。读完本文,你将掌握useDrag的全部选项参数、三类典型用法(基础拖拽、自定义拖拽图像、数据传递),并能从源码层面理解其事件注册、draggable属性自动设置与监听器自动清理的完整实现机制。

简介:一行 Hook 启用元素拖拽

useDrag接收一个指向 HTML 元素的引用(通常来自 React 的useRef),并让该元素变为可拖拽。Hook 返回一个布尔值,表示元素当前是否正在被拖拽。

官方文档(docs/useDrag.md)给出的三点价值主张:

  • 负责将拖拽相关的事件监听器挂载到指定的目标元素上;
  • 负责在组件卸载时移除这些监听器;
  • 让你可以轻松实现可拖拽的业务逻辑。

按需引入方式与包名对应关系可在 package.json 的exports字段中确认:./useDrag同时提供 ESM(dist/esm/useDrag.js)、CJS(dist/useDrag.js)与类型声明(dist/useDrag.d.ts)三种产物,因此支持beautiful-react-hooks/useDrag这种按子路径的引入方式,也便于打包器做 tree-shaking。包的peerDependencies要求react >= 18.2.0 < 20.0.0,使用时需保证项目 React 版本在该区间内。

基础用法:让元素可拖拽并跟踪拖拽状态

第一个参数传入一个 DOM ref,返回值即拖拽状态:

import { useRef } from 'react'; import useDrag from 'beautiful-react-hooks/useDrag'; const MyComponent = () => { const ref = useRef(); const isDragged = useDrag(ref); return ( <DisplayDemo title="useDrag"> <div ref={ref} style={{ padding: '20px 0', background: isDragged ? '#BE496E' : '#1D6C8B' }}> Draggable item... {isDragged && <span>is being dragged</span>} </div> </DisplayDemo> ); }; <MyComponent />

示例中的DisplayDemo是官方文档演示用的容器组件,实际项目中可替换为任意容器或直接去掉。

这里有两个值得注意的机制,均能由源码证实:

  1. 无需手动设置draggable属性。useDrag内部固定以isDraggable = true调用useDragEvents(见 src/useDrag.ts),而useDragEvents在挂载时的useEffect中检查目标元素:若其没有draggable属性,则自动调用setAttribute('draggable', 'true')(见 src/useDragEvents.ts)。因此示例代码没有写draggable属性也能拖,同时已自带该属性的元素不会被覆盖。
  2. isDragged只在dragstart与dragend两个时刻翻转。源码中onDragStart回调执行setIsDragging(true),onDragEnd回调执行setIsDragging(false)(见 src/useDrag.ts),它只回答"是否处于拖拽进行中",不提供拖拽坐标等更细粒度的信息——这一点在注意事项中会进一步展开。

自定义拖拽图像:dragImage 与偏移量

默认情况下,浏览器拖拽时显示的是元素自身的截图。如果希望拖拽时跟随鼠标的是一个自定义图片(例如项目 Logo 或缩略图),可以传入dragImage及其在光标处的偏移量:

import { useRef } from 'react'; import useDrag from 'beautiful-react-hooks/useDrag'; const MyComponent = () => { const ref = useRef(); const isDragged = useDrag(ref, { dragImage: 'https://beautifulinteractions.com/img/logo-colorful.svg', dragImageXOffset: 5, dragImageYOffset: 5, }); return ( <DisplayDemo title="useDrag"> <div ref={ref} style={{ padding: '20px 0', background: isDragged ? '#BE496E' : '#1D6C8B' }}> Draggable item... {isDragged && <span>is being dragged</span>} </div> </DisplayDemo> ); }; <MyComponent />

从源码看(src/useDrag.ts),这段配置的落地逻辑是:

if (opts.dragImage && event.dataTransfer) { const img = new Image() img.src = opts.dragImage event.dataTransfer.setDragImage(img, opts.dragImageXOffset ?? 0, opts.dragImageYOffset ?? 0) }

即在dragstart事件中动态构造一个Image对象并交给dataTransfer.setDragImage()渲染。两个偏移量的默认值均为0(由defaultOptions提供,见 src/useDrag.ts),其语义是图像左上角相对光标的像素偏移:示例中的5, 5让图像中心大致对准光标位置,避免图像左上角与光标重叠导致的"贴边"观感。

拖拽数据传递:transfer 与 transferFormat

HTML5 拖放模型中,"可拖拽源"负责往DataTransfer里写数据,"放置目标"在drop事件中读取。useDrag通过transfer与transferFormat两个选项封装了这个过程:

import { useRef } from 'react'; import useDrag from 'beautiful-react-hooks/useDrag'; const MyComponent = () => { const ref = useRef(); const isDragged = useDrag(ref, { transfer: { id: 'item-id', foo: 'bar' }, transferFormat: 'text/plain', }); return ( <DisplayDemo title="useDrag"> <div ref={ref} style={{ padding: '20px 0', background: isDragged ? '#BE496E' : '#1D6C8B' }}> Draggable item... {isDragged && <span>is being dragged</span>} </div> </DisplayDemo> ); }; <MyComponent />

源码中的序列化与写入逻辑(src/useDrag.ts)为:

if (opts.transfer && event.dataTransfer) { const data = typeof opts.transfer === 'object' ? JSON.stringify(opts.transfer) : `${opts.transfer}` event.dataTransfer.setData(opts.transferFormat ?? 'text', data) }

由此可以明确transfer的类型与行为对应关系:

  • 传入对象(如{ id: 'item-id', foo: 'bar' })时,会被JSON.stringify序列化为 JSON 字符串后写入;
  • 传入字符串或数字时,直接以模板字符串转成文本写入;
  • transferFormat指定写入DataTransfer的 MIME 类型,源码兜底值为'text'(defaultOptions.transferFormat,见 src/useDrag.ts);官方示例中显式使用了标准的'text/plain'。

对应的放置目标侧,可以用同库的 useDropZone 或在放置区的onDrop中通过event.dataTransfer.getData('text/plain')取回该字符串并JSON.parse还原。由于写入的始终是字符串,"对象 → JSON 字符串 → 放置端解析"这一约定需要两端自行保持一致,这是 HTML5 原生拖放模型本身的限制,并非 Hook 缺陷。

参数参考(Types)

完整选项接口与函数签名如下(继承自 docs/useDrag.md 的 Types 部分,与 src/useDrag.ts 的源码定义一致):

import { type RefObject } from 'react'; export interface UseDragOptions { dragImage?: string; dragImageXOffset?: number; dragImageYOffset?: number; transfer?: string | number | Record<string, any>; transferFormat?: string; } declare const useDrag: <TElement extends HTMLElement>(targetRef: RefObject<TElement>, options?: UseDragOptions) => boolean; export default useDrag;

结合源码defaultOptions与setData的兜底逻辑,各参数的默认值与语义整理如下:

参数类型默认值说明
dragImagestring无(不设置则使用元素自身截图)拖拽时显示的自定义图像 URL
dragImageXOffsetnumber0自定义图像相对光标的水平偏移(像素)
dragImageYOffsetnumber0自定义图像相对光标的垂直偏移(像素)
transferstring \| number \| Record<string, any>无(不写数据)拖拽时写入DataTransfer的数据;对象会被 JSON 序列化
transferFormatstring'text'写入数据所用的 MIME 格式,建议显式使用'text/plain'等标准类型

另外从源码结构看(src/useDrag.ts),选项的合并方式是{ ...defaultOptions, ...(options || {}) },即未显式传入的字段会自动补齐默认值;每个渲染都会重新注册捕获最新opts的回调,因此后续渲染中更新选项对象,对之后的下一次拖拽会生效。

源码走读:useDrag 底层是如何工作的

useDrag的实现不到 50 行,其核心是"三层委托"结构,理解这条调用链能解释它的行为边界。

第一层:useDrag(src/useDrag.ts)负责业务语义——状态翻转、dragImage设置、DataTransfer写入,全部集中在onDragStart/onDragEnd两个回调里。

第二层:useDragEvents(src/useDragEvents.ts)负责事件抽象。它基于useEvent批量构造了onDrag、onDrop、onDragEnter、onDragEnd、onDragExit、onDragLeave、onDragOver、onDragStart共 8 个回调设置器,并以Object.freeze返回只读对象(见 src/useDragEvents.ts)。它还包含一道防御性校验:若传入的 ref 不是标准的 React ref(缺少current属性),会直接抛出Unable to assign any drag event to the given ref错误(src/useDragEvents.ts),把"传错 ref"的问题暴露在渲染期而非事件期。

第三层:useEvent(src/useEvent.ts)负责真正的事件绑定。它在useEffect中执行addEventListener,并在 cleanup 函数中执行removeEventListener(见 src/useEvent.ts)——这正是文档中"组件卸载时自动移除监听器"承诺的实现来源。事件触发时,原生事件会被转发到一个 ref 中缓存的最新回调,而这个缓存机制由工厂函数 createHandlerSetter 提供。

这里有一个对使用者很关键的设计约束:createHandlerSetter的注释明确说明,它返回的 setter 只是更新回调 ref,"设置回调 ref 不会强制组件重新渲染"。因此回调必须在函数组件体内同步调用(onDragStart(fn)、onDragEnd(fn)写在 render 过程中),而不能放在setTimeout、Promise.then等异步上下文中——异步调用发生在事件监听建立流程之外,回调无法被正确挂载。这也是同库useDragEvents文档反复强调的"不要异步调用回调设置器"的原因。从源码结构看,这种"渲染期注册 + 事件期读取 ref"的模式同时避免了为每次拖拽重新绑定原生监听器,是这套 Hooks 控制性能的手段之一。

仓库内的测试用例 test/useDrag.spec.js 验证了基本契约:以{ current: document.createElement('div') }作为 ref 渲染 Hook 后,返回值必须是布尔值:

it('should return an object the state of the current dragging element', () => { const targetRef = { current: document.createElement('div') } const { result } = renderHook(() => useDrag(targetRef)) expect(result.current).to.be.an('boolean') })

注意事项与能力边界

  • 它封装的是 HTML5 原生拖放,而非基于 Pointer 事件的自定义拖拽。useDrag依赖dragstart/dragend事件与draggable属性,因此受浏览器原生拖放模型的约束(例如移动端浏览器对 HTML5 拖放的支持并不完整)。它适合"把元素拖到另一个放置区"这类场景,不适合实现需要逐帧跟手坐标的拖动交互。
  • isDragged只表达状态,不表达位置。Hook 不暴露clientX/clientY或偏移量,如果业务需要"元素跟着鼠标走"的效果,需要自行在drag事件基础上扩展,或直接使用同库的 useDragEvents(其文档中还给出了带放置区onDragOver/onDrop的完整示例)。
  • 放置端需要配对 Hook。transfer写入的数据必须有一个读取方,仓库中对应的放置端 Hook 是 useDropZone;若只使用useDrag,拖拽行为本身不受影响,只是drop时读不到数据。
  • 不建议用底层事件 Hook 替代标准 props:useDragEvents官方文档特别指出,若组件已有onDragStart={handler}这类标准 React 拖拽 props 的用法,应继续沿用标准 props 方案,因为绕过 React SyntheticEvent 会损失相应性能(见 docs/useDragEvents.md 的 "What not to do" 部分)。

何时选用 useDrag

官方文档给出的使用判据很简洁:

  • 当你需要基础的拖拽相关业务逻辑时。

结合源码与同库 Hook 的分工,可以把选型边界再具体化一些:

需求推荐 Hook
让元素可拖拽 + 跟踪"拖拽中"状态 + 可选自定义图像/传数据useDrag
需要drop、dragOver、dragEnter/Leave等完整事件集合来抽象自定义拖放逻辑useDragEvents
需要一个"放置区"来接收拖放数据useDropZone

useDrag本质上是useDragEvents的上层封装(仅消费其中的onDragStart/onDragEnd),因此当需求超出"状态翻转 + 图像 + 传数据"时,向下切换到底层 Hook 是仓库内自然的升级路径,而不必换用第三方拖拽库。

  • 前端
  • 开发工具

【免费下载链接】beautiful-react-hooks

🔥 A collection of beautiful and (hopefully) useful React hooks to speed-up your components and hooks development 🔥

项目地址:https://gitcode.com/gh_mirrors/be/beautiful-react-hooks
点击查看免费下载
上一篇:Prompt2Model高级配置教程:自定义组件、API集成与多平台部署
下一篇:【亲测免费】 强大的动态壁纸工具:SwayFX

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询