react-use 之 useLongPress:基于 React Hooks 实现长按手势检测的完整指南
【免费下载链接】react-useReact Hooks — 👍项目地址: https://gitcode.com/gh_mirrors/re/react-use
useLongPress是 react-use 传感器(Sensors)类 Hooks 家族的一员(见 README.md 中的 Sensors 分类),用于检测某个元素上的"长按"手势:当用户在元素上按下并持续按住超过指定延迟后,触发一次回调。本文以官方文档 docs/useLongPress.md 为主体,结合仓库源码 src/useLongPress.ts 与测试用例 tests/useLongPress.test.tsx,完整讲解它的用法、API 参数、底层实现原理与实战注意事项,读完即可在桌面端与移动端项目中落地使用。
一、useLongPress 是什么
useLongPress是一个 React 传感器 Hook,其官方定义是:"React sensor hook that fires a callback after long pressing",即在用户长按之后触发回调。
它并不自行绑定任何全局事件,而是返回一组事件处理函数(onMouseDown、onTouchStart、onMouseUp、onMouseLeave、onTouchEnd),由你把它们展开(spread)到目标 DOM 元素上。这种"把事件处理函数交给你"的设计,使它既能用于<button>等原生元素,也能用于任意自定义组件,同时不会在组件卸载时留下全局监听器。
二、安装与引入
react-use 是一个 npm 包,安装后即可按需引入单个 Hook:
npm i react-use在你的组件中引入:
import { useLongPress } from 'react-use';从源码 src/index.ts 可以看到,useLongPress作为命名导出(export { default as useLongPress } from './useLongPress')对外暴露,与文档中的引入方式一致。
三、基本用法
这是官方文档给出的完整示例:在按钮上长按 300ms 后触发回调。
import { useLongPress } from 'react-use'; const Demo = () => { const onLongPress = () => { console.log('calls callback after long pressing 300ms'); }; const defaultOptions = { isPreventDefault: true, delay: 300, }; const longPressEvent = useLongPress(onLongPress, defaultOptions); return <button {...longPressEvent}>useLongPress</button>; };关键点:
useLongPress的第一个参数是长按触发时要执行的回调函数;- 第二个参数是可选的配置对象,这里显式传入了
isPreventDefault: true与delay: 300(这两者恰好也是默认值); - 返回值是一个包含 5 个事件处理函数的对象,通过
{...longPressEvent}展开到<button>上,React 会将其绑定为按钮的对应事件。
仓库中的 Storybook 演示 stories/useLongPress.story.tsx 使用了完全相同的写法,可作为运行参考。
四、API 参考与参数详解
官方文档给出了完整的类型签名:
const { onMouseDown, onTouchStart, onMouseUp, onMouseLeave, onTouchEnd } = useLongPress( callback: (e: TouchEvent | MouseEvent) => void, options?: { isPreventDefault?: true, delay?: 300 } )返回值:5 个事件处理函数
| 处理函数 | 触发时机 | 行为 |
|---|---|---|
onMouseDown | 鼠标按下 | 启动长按计时 |
onTouchStart | 触摸开始 | 启动长按计时 |
onMouseUp | 鼠标松开 | 取消计时 |
onMouseLeave | 鼠标移出元素 | 取消计时 |
onTouchEnd | 触摸结束 | 取消计时 |
参数说明
callback— 长按达到delay毫秒后触发的回调函数,类型为(e: TouchEvent | MouseEvent) => void,回调会收到触发长按的原始事件对象。options?— 可选配置对象,包含两个字段:isPreventDefault?— 是否在touchend事件上调用event.preventDefault(),用于在移动设备上阻止幽灵点击(ghost click),默认值为true。delay?— 长按判定延迟,单位毫秒,超过该时长才调用回调,默认值为300。
需要说明的是,文档签名中options的类型写为{ isPreventDefault?: true, delay?: 300 }(字面量形式),而源码 src/useLongPress.ts 中的真实类型是更宽松的{ isPreventDefault?: boolean; delay?: number },两个字段都是可选的、可传入任意布尔值/数字。
五、源码实现剖析:长按是如何被检测的
理解底层实现有助于你正确使用和调试。useLongPress的核心逻辑只有两个闭包函数:start(启动计时)与clear(取消计时),见 src/useLongPress.ts。
1. start:按下即开始计时
const start = useCallback( (event: TouchEvent | MouseEvent) => { // prevent ghost click on mobile devices if (isPreventDefault && event.target) { on(event.target, 'touchend', preventDefault, { passive: false }); target.current = event.target; } timeout.current = setTimeout(() => callback(event), delay); }, [callback, delay, isPreventDefault] );当onMouseDown或onTouchStart被触发时:
- 若开启了
isPreventDefault,则通过工具函数on(定义在 src/misc/util.ts)在当前event.target上动态注册touchend监听器,并把目标元素暂存到target.current备用。注意这里显式传入了{ passive: false }——因为preventDefault需要非 passive 的监听器才能生效。 - 调用
setTimeout启动计时,delay毫秒后执行传入的callback(event),并且回调收到的是触发长按的那个原始事件对象。
2. clear:松开/移出即取消计时
const clear = useCallback(() => { // clearTimeout and removeEventListener timeout.current && clearTimeout(timeout.current); if (isPreventDefault && target.current) { off(target.current, 'touchend', preventDefault); } }, [isPreventDefault]);当onMouseUp、onMouseLeave或onTouchEnd触发时:
- 若计时器尚未到期,
clearTimeout将其取消,回调不会执行; - 若之前注册过
touchend监听,则通过工具函数off(见 src/misc/util.ts)将其移除,避免在元素上残留监听器。
3. 为什么两个 ref
useLongPress使用useRef保存计时器句柄(timeout)和事件目标(target),而不是useState。原因在于:
- 这些值不需要触发重新渲染,用 ref 更高效;
- 计时器句柄必须在多次事件回调之间共享,ref 能在不引发重渲染的前提下跨渲染周期保持同一引用。
4. 事件处理函数的组装
最后,Hook 将上述逻辑包装成五个处理函数并返回:
return { onMouseDown: (e: any) => start(e), onTouchStart: (e: any) => start(e), onMouseUp: clear, onMouseLeave: clear, onTouchEnd: clear, } as const;可以看到,onMouseDown与onTouchStart共用start,onMouseUp、onMouseLeave、onTouchEnd共用clear,这正是"按下启动、松开/离开/结束取消"这一长按判定模型的直接体现。
六、移动端幽灵点击(Ghost Click)与 preventDefault 细节
这是useLongPress面向移动端最重要的工程细节,也是isPreventDefault选项存在的意义。
在部分移动浏览器中,长按会触发一系列后续事件(如文本选择菜单、或模拟点击的 ghost click),造成意外的交互副作用。为此源码实现了专门的事件过滤器:
const isTouchEvent = (ev: Event): ev is TouchEvent => { return 'touches' in ev; }; const preventDefault = (ev: Event) => { if (!isTouchEvent(ev)) return; if (ev.touches.length < 2 && ev.preventDefault) { ev.preventDefault(); } };三个值得注意的细节:
- 只拦截触摸事件:
isTouchEvent通过'touches' in ev判断事件是否为TouchEvent,鼠标事件不会被误拦截; - 多指手势放行:
ev.touches.length < 2意味着当检测到双指及以上的触摸时不阻止默认行为,避免干扰双指缩放等原生手势; - 动态注册与卸载:监听器在
start时绑定、在clear时移除,生命周期完全与一次长按交互绑定,不会常驻。
需要说明的是,官方文档将其描述为"阻止移动设备上某些情况下的幽灵点击"(preventing ghost click on mobile devices in some cases),属于尽力而为的防御性处理,不保证覆盖所有浏览器行为。
七、行为验证:测试用例说明了什么
仓库测试 tests/useLongPress.test.tsx 使用@testing-library/react-hooks和 jest 假定时器(jest.useFakeTimers())验证了 Hook 的核心行为,可以帮助你精确理解触发与取消的边界:
- 无任何事件时不触发:仅渲染 Hook 而不触发任何事件,即使推进
defaultDelay(300ms)时间,回调调用次数仍为 0; - onMouseDown 后延时触发:
mousedown后推进300 - 20ms 不触发,再推进 20ms 恰好触发一次,验证delay的精确语义; - 自定义 delay 生效:传入
delay: 1000时,须推进满 1000ms 才会调用回调; - onMouseLeave 中断:
mousedown后未满 300ms 触发mouseleave,即使继续推进时间,回调也不会被调用; - onMouseUp 中断:
mousedown后提前mouseup,回调不会执行; - onTouchStart 触发、onTouchEnd 中断:触摸路径与鼠标路径行为一致。
这些用例共同确认了:长按回调只在"按住事件发生后、释放事件发生前"这段时间窗口内到达 delay 阈值时才触发——一旦提前松开、移出或结束触摸,计时即被取消。测试中expect(setTimeout).toHaveBeenCalledTimes(1)也印证了每次按下只启动一个计时器。
八、实战示例与进阶建议
示例 1:桌面端长按按钮触发操作
import { useLongPress } from 'react-use'; const DeleteButton = () => { const onLongPress = () => { // 长按 500ms 后执行删除 console.log('long pressed, do delete'); }; const bind = useLongPress(onLongPress, { delay: 500 }); return <button {...bind}>长按删除</button>; };示例 2:移动端长按图片弹出菜单(保留幽灵点击防护)
const ImageWithLongPress = ({ src }) => { const onLongPress = (e) => { // e 为触发长按的 TouchEvent 或 MouseEvent console.log('long press on image', e.target); }; // 保持 isPreventDefault 默认值 true,减少移动端 ghost click 干扰 const bind = useLongPress(onLongPress, { delay: 600 }); return <img src={src} alt="demo" {...bind} />; };进阶建议与注意事项
- 回调稳定性:
start通过useCallback依赖[callback, delay, isPreventDefault],若传入的回调在每次渲染时都是新引用(如内联箭头函数),start也会随之重建,属正常现象;如需极致优化可先用useCallback包裹业务回调。 - 触摸与鼠标的边界:返回的事件处理函数同时覆盖
Mouse与Touch事件族,在同时支持两者的设备上两者都会生效,按实际场景决定是否需要额外区分。 - 不要与原生长按语义混淆:此 Hook 不做文本选择、拖拽等原生行为的完整模拟,它只负责"按下-计时-触发/取消"这一手势判定;配合
isPreventDefault可缓解移动端原生长按副作用的干扰。 - 事件对象透传:回调收到的参数是启动长按计时时的原始事件,类型为
TouchEvent | MouseEvent,在移动端处理时可用文档/源码中的isTouchEvent思路('touches' in ev)自行判别事件来源。
九、总结
useLongPress用极简的 API(一个回调、两个可选参数、五个可展开的事件处理函数)封装了完整的长按手势检测:start负责按下时启动setTimeout计时并动态挂载touchend防幽灵点击监听,clear负责松开、移出或触摸结束时取消计时并清理监听。结合仓库测试所验证的精确触发/取消语义,你可以放心地将其用于按钮、图片、列表项等任意元素的长按交互,在桌面端与移动端获得一致且可控的体验。
【免费下载链接】react-useReact Hooks — 👍项目地址: https://gitcode.com/gh_mirrors/re/react-use
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考