Craft a Reusable useGesture Hook in React
This challenge asks you to build a custom React hook, useGesture, that abstracts away the complexities of handling various pointer and touch events on DOM elements. A well-designed useGesture hook can significantly simplify gesture recognition in React applications, leading to cleaner and more maintainable code for interactive UIs.
Problem Description
You need to create a custom React hook named useGesture that takes a configuration object as input and returns a ref object. This ref object should be attached to a DOM element, and the hook should automatically handle relevant pointer and touch events to provide gesture-specific callbacks.
Key Requirements:
- Event Handling: The hook should listen for common pointer and touch events:
pointerdown,pointermove,pointerup,touchstart,touchmove,touchend,mousedown,mousemove,mouseup. - Configuration Object: The hook should accept an optional configuration object. This object should allow users to define callbacks for specific gestures or events. Initially, focus on supporting
onPressandonDrag. onPressGesture: This callback should be triggered when a pointer/touch is pressed down on the element and then released without significant movement. Define a threshold for "significant movement" (e.g., 5 pixels).onDragGesture: This callback should be triggered when a pointer/touch is pressed down, moves a significant distance, and is then released. The callback should receive the current pointer/touch position relative to the initial press.- Ref Return: The hook should return a
RefObject<HTMLElement>which the user will attach to their target DOM element. - Cleanup: Ensure all event listeners are properly removed when the component unmounts or the hook's dependencies change.
- TypeScript: The hook and its types should be written in TypeScript for strong typing.
Expected Behavior:
When a user applies the useGesture hook to an element:
- Press: If the user taps and quickly releases without dragging more than the defined threshold, the
onPresscallback should be invoked. - Drag: If the user presses down and moves their pointer/touch beyond the defined threshold, the
onDragcallback should be invoked during the drag. Upon release, the drag gesture is considered complete.
Edge Cases to Consider:
- Multiple pointers/touches: For this initial challenge, you can simplify by only considering the primary pointer/touch.
- Preventing default browser behavior: For drag operations, you might want to consider preventing default behaviors like scrolling.
- Touch vs. Pointer events: Ensure your logic gracefully handles both. Pointer events are generally preferred as they unify mouse, touch, and pen interactions.
Examples
Example 1: Basic Press
import React, { useRef } from 'react';
import { useGesture } from './useGesture'; // Assuming your hook is in './useGesture'
function DraggableBox() {
const boxRef = useRef<HTMLDivElement>(null);
const handlePress = () => {
console.log('Box pressed!');
};
const { ref } = useGesture({ onPress: handlePress });
// Merge the returned ref with our own ref
const combinedRef = (element: HTMLDivElement | null) => {
ref(element);
boxRef.current = element;
};
return (
<div
ref={combinedRef}
style={{
width: '100px',
height: '100px',
backgroundColor: 'lightblue',
cursor: 'pointer',
}}
>
Press Me
</div>
);
}
Output (when the box is clicked and released quickly):
Box pressed!
Explanation:
The useGesture hook is attached to the div. When the div is clicked and released within the "press" threshold, the onPress callback is executed, logging a message to the console.
Example 2: Basic Drag
import React, { useRef, useState } from 'react';
import { useGesture, DragGestureState } from './useGesture'; // Assuming your hook is in './useGesture'
function DraggableItem() {
const itemRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleDrag = (state: DragGestureState) => {
setPosition({ x: state.deltaX, y: state.deltaY });
};
const { ref } = useGesture({ onDrag: handleDrag });
const combinedRef = (element: HTMLDivElement | null) => {
ref(element);
itemRef.current = element;
};
return (
<div
ref={combinedRef}
style={{
width: '50px',
height: '50px',
backgroundColor: 'lightgreen',
position: 'absolute',
left: `${position.x}px`,
top: `${position.y}px`,
cursor: 'grab',
}}
>
Drag Me
</div>
);
}
Output (when the item is dragged):
The div's left and top CSS properties will update to reflect the deltaX and deltaY from the initial press. If you were to inspect position state during a drag, it would continuously update.
Explanation:
The useGesture hook listens for drag events. As the user drags the div, the onDrag callback is called, updating the position state with the relative movement. This state change causes the div to re-render and visually follow the cursor.
Constraints
- The hook must be written in TypeScript.
- The
useGesturehook should return an object containing arefproperty. - The
onPressgesture should be considered active if the pointer/touch movement is less than or equal to5pixels from the initial press point. - Performance: The hook should be efficient and avoid unnecessary re-renders. Event listeners should be added and removed judiciously.
Notes
- Consider using
pointerdownas the primary event to initiate gesture detection, as it unifies mouse and touch events. - You'll need to manage internal state within the hook to track whether a gesture is in progress, the initial press coordinates, and the current movement deltas.
- Think about how to correctly merge the ref returned by your hook with the ref provided by the user. The
useRefhook in React typically returns aMutableRefObject. Your hook should return a compatible ref that the user can assign. - For
onDrag, thedeltaXanddeltaYshould be relative to the initial press point, not the previous move event.