Created
October 15, 2025 13:38
-
-
Save joshwcomeau/5b110813270f8ec5d07ab9c3a4182228 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| I frequently need to track the user’s mouse position, which means re-rendering 60+ times a second when they move the mouse. | |
| Typically, I only care about the mouse position when it’s near a particular element (eg. the "Like" button tilting at the cursor when it’s nearby). This hook allows us to track the mouse position, but only re-render when the mouse is near the element. The global event handler still fires on every mousemove, but we skip the state update if it’s not near the element. | |
| When the cursor is not near the element, x/y are both set to `null`. | |
| */ | |
| import * as React from 'react'; | |
| import { getDistanceBetweenPoints, clamp } from '@utils'; | |
| interface MousePosition { | |
| x: number | null; | |
| y: number | null; | |
| } | |
| function useMousePositionWhenNearElement({ | |
| boundingBox, | |
| radius, | |
| // By default, this hook returns the raw `clientX`/`clientY` values, but I can optionally specify that they should be relative to the top-left corner of `boundingBox`. | |
| relativeToElement, | |
| // When the mouse moves far away from the element, the default behaviour is to revert to `null` for x/y, but we can optionally keep the final known position | |
| preserveFinalPosition, | |
| // By default, the mouse position will be clamped to the bounding box. So, if the cursor moves to the left of the element, the x value will be set to 0. | |
| clampToBoundingBox = true, | |
| }: { | |
| boundingBox: DOMRect | null; | |
| radius: number; | |
| relativeToElement?: boolean; | |
| preserveFinalPosition?: boolean; | |
| clampToBoundingBox?: boolean; | |
| }) { | |
| const [mousePosition, setMousePosition] = | |
| React.useState<MousePosition>({ | |
| x: null, | |
| y: null, | |
| }); | |
| const cachedMousePosition = | |
| React.useRef<MousePosition>(mousePosition); | |
| cachedMousePosition.current = mousePosition; | |
| React.useEffect(() => { | |
| const updateMousePosition = (ev: MouseEvent) => { | |
| if (!boundingBox) { | |
| return; | |
| } | |
| const mousePoint = { x: ev.clientX, y: ev.clientY }; | |
| const centerPoint = { | |
| x: boundingBox.left + boundingBox.width / 2, | |
| y: boundingBox.top + boundingBox.height / 2, | |
| }; | |
| const distance = getDistanceBetweenPoints( | |
| mousePoint, | |
| centerPoint | |
| ); | |
| if (distance < radius) { | |
| setMousePosition(mousePoint); | |
| } else if (!preserveFinalPosition) { | |
| // We want to set the mouse position to null if the mouse is outside the radius, but we don't want to cause re-renders on every single mouse-move on the other side of the document. Use the cached mouse position to see if we need to change anything | |
| if ( | |
| cachedMousePosition.current.x !== null || | |
| cachedMousePosition.current.y !== null | |
| ) { | |
| setMousePosition({ x: null, y: null }); | |
| } | |
| } | |
| }; | |
| window.addEventListener('mousemove', updateMousePosition); | |
| return () => | |
| window.removeEventListener('mousemove', updateMousePosition); | |
| }, [boundingBox, radius]); | |
| if ( | |
| relativeToElement && | |
| boundingBox && | |
| mousePosition.x !== null && | |
| mousePosition.y !== null | |
| ) { | |
| return { | |
| x: clampToBoundingBox | |
| ? clamp( | |
| mousePosition.x - boundingBox.left, | |
| 0, | |
| boundingBox.width | |
| ) | |
| : mousePosition.x - boundingBox.left, | |
| y: clampToBoundingBox | |
| ? clamp( | |
| mousePosition.y - boundingBox.top, | |
| 0, | |
| boundingBox.height | |
| ) | |
| : mousePosition.y - boundingBox.top, | |
| }; | |
| } | |
| return mousePosition; | |
| } | |
| export default useMousePositionWhenNearElement; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment