- 前端
- 开发工具
【免费下载链接】beautiful-react-hooks
🔥 A collection of beautiful and (hopefully) useful React hooks to speed-up your components and hooks development 🔥
在 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是官方文档演示用的容器组件,实际项目中可替换为任意容器或直接去掉。
这里有两个值得注意的机制,均能由源码证实:
- 无需手动设置
draggable属性。useDrag内部固定以isDraggable = true调用useDragEvents(见 src/useDrag.ts),而useDragEvents在挂载时的useEffect中检查目标元素:若其没有draggable属性,则自动调用setAttribute('draggable', 'true')(见 src/useDragEvents.ts)。因此示例代码没有写draggable属性也能拖,同时已自带该属性的元素不会被覆盖。 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的兜底逻辑,各参数的默认值与语义整理如下:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
dragImage | string | 无(不设置则使用元素自身截图) | 拖拽时显示的自定义图像 URL |
dragImageXOffset | number | 0 | 自定义图像相对光标的水平偏移(像素) |
dragImageYOffset | number | 0 | 自定义图像相对光标的垂直偏移(像素) |
transfer | string \| number \| Record<string, any> | 无(不写数据) | 拖拽时写入DataTransfer的数据;对象会被 JSON 序列化 |
transferFormat | string | '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 🔥
相关推荐
Clypra基础操作指南:10分钟学会视频剪辑的核心技巧
Clypra基础操作指南:10分钟学会视频剪辑的核心技巧 Clypra是一款基于Tauri、React和TypeScript构建的现代视频编辑器,专注于提供免费
音视频视频视频处理桌面应用React Beautiful DND 拖拽元素的自定义形状:非矩形拖拽区域实现
React Beautiful DND 拖拽元素的自定义形状:非矩形拖拽区域实现 在使用React Beautiful DND构建拖放界面时,默认的矩形拖拽区域
自定义拖拽图层实战:用 react-dnd 的 DragLayer 打造零闪烁的拖拽反馈
自定义拖拽图层实战:用 react dnd 的 DragLayer 打造零闪烁的拖拽反馈 react dnd 的浏览器后端依赖 HTML5 原生拖放 API,而
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考