React usePointer Hook: Track Mouse Coordinates
Create a custom React hook, usePointer, that efficiently tracks the current mouse coordinates (X and Y) within a specified DOM element or the entire document. This hook will be invaluable for building interactive UI components that respond to user mouse movements, such as custom cursors, drag-and-drop interfaces, or visual feedback based on pointer position.
Problem Description
Your task is to implement a TypeScript React hook named usePointer. This hook should expose the current X and Y coordinates of the mouse pointer.
Key Requirements:
- The hook should accept an optional
ref(a ReactRefObject) to a DOM element. - If a
refis provided, the hook should only track mouse movements within that specific element. - If no
refis provided, the hook should track mouse movements across the entiredocument. - The hook should return an object containing
xandyproperties, representing the current mouse coordinates. - The hook should handle cleanup to remove event listeners when the component unmounts or the
refchanges. - Coordinates should be relative to the top-left corner of the target element (or the document if no element is specified).
Expected Behavior:
- When the mouse moves over the target element (or the document), the hook should update its state with the latest
xandycoordinates. - When the mouse leaves the target element, the coordinates should ideally reset or remain at their last recorded position within the element (depending on your chosen implementation strategy for leaving). For this challenge, let's assume they should reset to
nullor a default value when the mouse leaves the bounded area. - The hook should be performant and avoid unnecessary re-renders.
Edge Cases:
- What happens if the
refis initiallynullor undefined? - What happens if the mouse enters and leaves the target element rapidly?
- Consider touch events as a potential extension, though not strictly required for this initial challenge.
Examples
Example 1: Tracking within a specific element
import React, { useRef } from 'react';
import usePointer from './usePointer'; // Assuming usePointer is in './usePointer'
function InteractiveBox() {
const boxRef = useRef<HTMLDivElement>(null);
const { x, y } = usePointer(boxRef);
return (
<div
ref={boxRef}
style={{
width: '200px',
height: '200px',
border: '1px solid black',
position: 'relative', // Important for coordinate understanding
overflow: 'hidden',
}}
>
{x !== null && y !== null ? (
<p style={{ position: 'absolute', top: y, left: x }}>Pointer is here!</p>
) : (
<p>Move your mouse inside the box.</p>
)}
<p>
Coordinates (relative to box): X: {x ?? 'N/A'}, Y: {y ?? 'N/A'}
</p>
</div>
);
}
Input: User moves the mouse inside the InteractiveBox.
Output:
The <p> element inside the box will follow the mouse.
The Coordinates (relative to box) paragraph will display the current x and y values, updating as the mouse moves. For instance, if the mouse is at the center of the box, X: 100, Y: 100 (assuming the box is 200x200). If the mouse leaves the box, X: N/A, Y: N/A.
Explanation: The usePointer hook is passed boxRef. It listens for mousemove events on the boxRef.current element. The returned x and y are offsets from the top-left corner of the div.
Example 2: Tracking within the entire document
import React from 'react';
import usePointer from './usePointer'; // Assuming usePointer is in './usePointer'
function GlobalTracker() {
const { x, y } = usePointer(); // No ref passed
return (
<div>
<h1>Global Mouse Tracker</h1>
<p>
Current Mouse Position (relative to document): X: {x ?? 'N/A'}, Y: {y ?? 'N/A'}
</p>
<div style={{ height: '100vh', backgroundColor: '#f0f0f0' }}>
Content to show scrolling doesn't affect tracking
</div>
</div>
);
}
Input: User moves the mouse anywhere on the page.
Output:
The Current Mouse Position paragraph will display the x and y coordinates relative to the top-left corner of the browser window's viewport. If the mouse is at the very top-left, X: 0, Y: 0.
Explanation: Since no ref is provided to usePointer, it attaches event listeners to the document object. The returned x and y are relative to the document's top-left corner.
Example 3: Handling initial state and cleanup
If a component using usePointer(ref) unmounts, or the ref becomes null (e.g., the element is conditionally rendered and removed), the event listeners should be properly removed to prevent memory leaks. The hook should manage this lifecycle.
Constraints
- The hook must be written in TypeScript.
- Use React hooks (
useState,useEffect,useRef). - Event listeners should be attached efficiently. Avoid attaching listeners on every render.
- The hook should be tested for proper cleanup of event listeners.
- The coordinates should be numbers, or
nullif not currently tracking (e.g., mouse outside the bounds or before the first event).
Notes
- Consider the difference between
clientX/clientYandpageX/pageY. For coordinates relative to an element,clientX/clientYare generally more suitable, and you'll need to calculate the offset from the element's bounding rectangle. - The
useRefhook is essential for passing the DOM element to the hook. - Think about how to handle the state updates to prevent excessive re-renders if only one coordinate changes.
- The initial state of
xandyshould benullor a sensible default before any mouse events occur.