import React from 'react';
const useMousePosition = () => {
const [
mousePosition,
setMousePosition
] = React.useState({ x: null, y: null });
React.useEffect(() => {
const updateMousePosition = ev => {
setMousePosition({ x: ev.clientX, y: ev.clientY });
};
window.addEventListener('mousemove', updateMousePosition);
return () => {
window.removeEventListener('mousemove', updateMousePosition);
};
}, []);
return mousePosition;
};
export default useMousePosition;
For certain interactions, you need to know exactly where the user's cursor is.
For example, on this blog, I have a “like” button which responds to the user's cursor position:
useMousePosition
is a low-level hook used in effects like these. It measures the user's current mouse position, in pixels, from the top/left corner. It stores this data in React state, and updates it whenever the cursor moves.
Because it's held in React state, the component will re-render whenever the user moves the mouse, and so you can safely use it to calculate things like CSS transform values, canvas animations, etc.
Link to this headingExample usage:
Code Playground
JSX
Result
Link to this headingPerformance
This component will re-render whenever the user moves the mouse. This can be dozens and dozens of times a second.
Originally, this hook included “throttle” functionality, which would limit the updates to a user-specified interval. In testing, though, it seemed to make performance worse. No matter how hard I tried, I couldn't come up with a contrived scenario where the throttle actually improved performance (while still updating often enough for smooth animations).
That said, you do still need to be a bit careful where you use this hook. It shouldn't be used in a top-level component like App
or Homepage
, since that will cause a huge chunk of your React tree to re-render very often. Use this hook in the small “leaf node” components near the bottom of the tree.
For maximum performance, you can use a library like React Spring or Framer Motion, which will allow you to update values without triggering React renders. In my experience, though, as long as you're using this hook on smaller components that don't have a big DOM impact, you should be just fine.
Link to this headingAccessibility
If you're going to be using this hook to animate an element's position, be sure to consider the user's motion preferences!
You can check with the use-prefers-reduced-motion hook. For example:
function CursorBox() {
const mousePosition = useMousePosition();
const prefersReducedMotion = usePrefersReducedMotion();
const transform = prefersReducedMotion
? null
: `translate(${mousePosition.x}px)`;
return (
<div
className="cursor-box"
style={{
transform,
}}
/>
);
}
Link to this headingTouchscreens and mobile
This hook is written to respond exclusively to mouse events. This means that it'll ignore the movement of fingers across a touchscreen.
In my experience, I generally don't want to handle touch events in the same way. Dragging a finger across the screen is more like scrolling than it is like moving a mouse.
I mainly use this hook for cosmetic desktop-only effects, like the 404 error page(opens in new tab) on this blog.
For those curious, though, here's how I'd write this hook if it needed to also track touch position:
import React from 'react';
const useMousePosition = ({ includeTouch }) => {
const [
mousePosition,
setMousePosition
] = React.useState({ x: null, y: null });
React.useEffect(() => {
const updateMousePosition = ev => {
let x, y;
if (ev.touches) {
const touch = ev.touches[0];
[x, y] = [touch.clientX, touch.clientY];
} else {
[x, y] = [ev.clientX, ev.clientY];
}
setMousePosition({ x, y });
};
window.addEventListener('mousemove', updateMousePosition);
if (includeTouch) {
window.addEventListener('touchmove', updateMousePosition);
}
return () => {
window.removeEventListener('mousemove', updateMousePosition);
if (includeTouch) {
window.removeEventListener('touchmove', updateMousePosition);
}
};
}, [includeTouch]);
return mousePosition;
};
export default useMousePosition;
Last updated on
March 20th, 2022