useHover
Prompt
Create a custom hook useHover that takes a ref and returns a boolean indicating whether the element is currently being hovered. The hook should attach mouseenter and mouseleave event listeners and clean them up on unmount.
Playground
You need a boolean state (useState) and a useEffect to
attach event listeners to the element. mouseenter sets
it to true, mouseleave sets it to false.
Inside useEffect, grab the DOM element from
ref.current. Add the event listeners, and return a
cleanup function that removes them. Don't forget the early
return if ref.current is null.
Solution
Explanation
Once you understand how this hook works, you'll notice the same pattern everywhere in React: hold some state, set up event listeners in useEffect, clean them up when the component goes away.
export function useHover(elementRef) {
const [value, setValue] = useState(false);
const handleMouseEnter = () => setValue(true);
const handleMouseLeave = () => setValue(false);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
element.addEventListener(
'mouseenter',
handleMouseEnter
);
element.addEventListener(
'mouseleave',
handleMouseLeave
);
return () => {
element.removeEventListener(
'mouseenter',
handleMouseEnter
);
element.removeEventListener(
'mouseleave',
handleMouseLeave
);
};
}, [elementRef]);
return value;
}The hook receives a ref that points to a DOM element. Inside useEffect, we grab the actual element from ref.current and attach two listeners: mouseenter flips state to true, mouseleave flips it back to false. Then we return value so the consuming component knows whether the element is being hovered right now.
The if (!element) return guard is important. The ref might be null if the element hasn't mounted yet or has been conditionally removed from the DOM. Without this check, you'd get an error trying to call addEventListener on null.
The cleanup function (the function we return from useEffect) removes both listeners when the component unmounts. This prevents a common bug: without cleanup, the listeners would keep running even after the component is gone, trying to update state on something that no longer exists. React would warn you about this with the "can't perform a state update on an unmounted component" message.
This pattern (state + useEffect + listeners + cleanup) is the backbone of custom hooks that interact with the DOM. You'll see it again in useClickOutside, useScrollPosition, useIdle, and many others. Once you've built one, building the rest feels natural.