useLongPress
EditRecognizes a pointer press held for a configured duration.
import { useCallback, useEffect, useLayoutEffect, useRef,} from "react";
import type { PointerEventHandler } from "react";
export interface UseLongPressOptions { delayMs?: number; disabled?: boolean; movementThreshold?: number; onCancel?: (this: void) => void; onFinish?: (this: void) => void; onStart?: (this: void) => void;}
export interface LongPressHandlers<ElementType extends HTMLElement> { onPointerCancel: PointerEventHandler<ElementType>; onPointerDown: PointerEventHandler<ElementType>; onPointerLeave: PointerEventHandler<ElementType>; onPointerMove: PointerEventHandler<ElementType>; onPointerUp: PointerEventHandler<ElementType>;}
interface ActiveLongPress { pointerId: number; startX: number; startY: number; timer?: ReturnType<typeof setTimeout>; triggered: boolean;}
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
/** Returns pointer handlers that recognize a press held for a configured delay. */export const useLongPress = <ElementType extends HTMLElement = HTMLElement>( handler: (this: void) => void, options: UseLongPressOptions = {},): LongPressHandlers<ElementType> => { const { delayMs = 500, disabled = false, movementThreshold = 10, onCancel, onFinish, onStart, } = options; if (!Number.isFinite(delayMs) || delayMs < 0) { throw new RangeError("delayMs must be a finite, non-negative number"); } if (!Number.isFinite(movementThreshold) || movementThreshold < 0) { throw new RangeError("movementThreshold must be a finite, non-negative number"); }
const callbacksRef = useRef({ handler, onCancel, onFinish, onStart }); useIsomorphicLayoutEffect(() => { callbacksRef.current = { handler, onCancel, onFinish, onStart }; }, [handler, onCancel, onFinish, onStart]); const stateRef = useRef<ActiveLongPress | null>(null);
const cancel = useCallback((finished: boolean) => { const state = stateRef.current; if (state === null) return; if (state.timer !== undefined) clearTimeout(state.timer); stateRef.current = null; const { onCancel: currentOnCancel, onFinish: currentOnFinish } = callbacksRef.current; if (state.triggered && finished) currentOnFinish?.(); else if (!state.triggered) currentOnCancel?.(); }, []);
useEffect( () => () => cancel(false), [cancel, delayMs, disabled, movementThreshold], ); useEffect(() => { if (disabled) cancel(false); }, [cancel, disabled]);
const onPointerDown = useCallback<PointerEventHandler<ElementType>>( (event) => { if (disabled || event.button !== 0) return; cancel(false); const currentOnStart = callbacksRef.current.onStart; currentOnStart?.(); const state: ActiveLongPress = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, triggered: false, }; state.timer = setTimeout(() => { state.triggered = true; const currentHandler = callbacksRef.current.handler; currentHandler(); }, delayMs); stateRef.current = state; }, [cancel, delayMs, disabled], ); const onPointerMove = useCallback<PointerEventHandler<ElementType>>( (event) => { const state = stateRef.current; if (state === null || event.pointerId !== state.pointerId) return; if ( Math.hypot(event.clientX - state.startX, event.clientY - state.startY) > movementThreshold ) { cancel(false); } }, [cancel, movementThreshold], ); const onPointerUp = useCallback<PointerEventHandler<ElementType>>( (event) => { if (stateRef.current?.pointerId === event.pointerId) cancel(true); }, [cancel], ); const onPointerCancel = useCallback<PointerEventHandler<ElementType>>( (event) => { if (stateRef.current?.pointerId === event.pointerId) cancel(false); }, [cancel], );
return { onPointerCancel, onPointerDown, onPointerLeave: onPointerCancel, onPointerMove, onPointerUp, };};Download
Section titled “Download”wget -O src/hooks/useLongPress.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/react-hooks/useLongPress.tstemp_dir="$(mktemp -d)" && bunx degit jrTilak/lazykit/registry/react-hooks "$temp_dir" && mv "$temp_dir/useLongPress.ts" src/hooks/useLongPress.ts && rm -r "$temp_dir"Examples
Section titled “Examples”import { useState } from "react";
import { useLongPress } from "./useLongPress";
export const LongPressExample = () => { const [count, setCount] = useState(0); const handlers = useLongPress<HTMLButtonElement>( () => setCount((value) => value + 1), { delayMs: 600 }, );
return ( <button type="button" {...handlers}> Hold to increment: {count} </button> );};handler— Runs after the press reachesdelayMs.options— Configure delay, disabled state, movement threshold, and lifecycle callbacks.
Returns
Section titled “Returns”LongPressHandlers<ElementType> — Pointer handlers to spread onto the target.