react-use 之 useLongPress:基于 React Hooks 实现长按手势检测的完整指南
2026/9/18 21:49:30 网站建设 项目流程

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",即在用户长按之后触发回调

它并不自行绑定任何全局事件,而是返回一组事件处理函数(onMouseDownonTouchStartonMouseUponMouseLeaveonTouchEnd),由你把它们展开(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: truedelay: 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] );

onMouseDownonTouchStart被触发时:

  1. 若开启了isPreventDefault,则通过工具函数on(定义在 src/misc/util.ts)在当前event.target上动态注册touchend监听器,并把目标元素暂存到target.current备用。注意这里显式传入了{ passive: false }——因为preventDefault需要非 passive 的监听器才能生效。
  2. 调用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]);

onMouseUponMouseLeaveonTouchEnd触发时:

  1. 若计时器尚未到期,clearTimeout将其取消,回调不会执行;
  2. 若之前注册过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;

可以看到,onMouseDownonTouchStart共用startonMouseUponMouseLeaveonTouchEnd共用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(); } };

三个值得注意的细节:

  1. 只拦截触摸事件isTouchEvent通过'touches' in ev判断事件是否为TouchEvent,鼠标事件不会被误拦截;
  2. 多指手势放行ev.touches.length < 2意味着当检测到双指及以上的触摸时阻止默认行为,避免干扰双指缩放等原生手势;
  3. 动态注册与卸载:监听器在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包裹业务回调。
  • 触摸与鼠标的边界:返回的事件处理函数同时覆盖MouseTouch事件族,在同时支持两者的设备上两者都会生效,按实际场景决定是否需要额外区分。
  • 不要与原生长按语义混淆:此 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),仅供参考

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

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

立即咨询