I have a div that is simply supposed to display 'HOVERING' if the cursor is hovering over it, and 'NOT HOVERING' otherwise. For some reason, it behaves as expected if I slowly hover each div on the page; however, if I quickly move my cursor across the screen, some of the divs bee switched. Meaning, they will display "NOT HOVERING" when my cursor moves over the div, and "HOVERING" when my cursor is not over the div.
This error occurs in both Chrome and Safari.
Sandbox:
=/src/Geo.js
Move your cursor quickly over the boxes to see the issue.
I have a div that is simply supposed to display 'HOVERING' if the cursor is hovering over it, and 'NOT HOVERING' otherwise. For some reason, it behaves as expected if I slowly hover each div on the page; however, if I quickly move my cursor across the screen, some of the divs bee switched. Meaning, they will display "NOT HOVERING" when my cursor moves over the div, and "HOVERING" when my cursor is not over the div.
This error occurs in both Chrome and Safari.
Sandbox:
https://codesandbox.io/s/aged-butterfly-r2g6x?file=/src/Geo.js
Move your cursor quickly over the boxes to see the issue.
Share Improve this question edited Aug 3, 2020 at 2:32 Evan Hessler asked Aug 3, 2020 at 0:40 Evan HesslerEvan Hessler 3371 gold badge4 silver badges24 bronze badges 2- 1 Can you provide a running codesandbox that reproduces the issue? Other than you should use a functional state update it isn't clear what may be occurring since we're only shown a single ponent and not the overall structure of what's rendered. – Drew Reese Commented Aug 3, 2020 at 0:47
- @DrewReese Yes, I can. Please see codesandbox.io/s/aged-butterfly-r2g6x?file=/src/Geo.js. I will add it to the post. – Evan Hessler Commented Aug 3, 2020 at 2:31
2 Answers
Reset to default 8Issue
I think the main issue with your implementation is with the way asynchronous event callbacks are queued up and processed in the event loop. I can't find any hard details about the latency of processing event callbacks but the docs here and here may shed some more light on the matter if you care to do a deep dive.
Basically the issue is two-fold:
- There is a minute duration a single event loop takes to process, i.e. detect an event and add it to the queue. I suspect the mouse is moving fast enough off/out the screen or into another div it isn't detected. The divs "jumping"/"moving" when hovering also doesn't help much.
- The ponent logic assumes all events can and will be detected and simply toggled the previous existing state. As soon as an event is missed though the toggling is inverted, thus the issue you see. Even in the updated sandbox this latency can cause one of the elements to get "stuck" hovered
Proposed Solution
Add a mouse move event listener to the window object and check if the mouse move event target is contained by one of your elements. If not currently hovered and element contains event target, set isHovered
true, and if currently hovered and the element does not contain event target, set isHovered
false.
This isn't a full replacement for the enter/leave|over/out event listeners attached to the containing div
as I was still able to reproduce an edge-case. I noticed your UI is most susceptible to this issue when moving the mouse quickly and leaving the window.
Combining the window and div event listeners gives a pretty good resolution (though I was still able to reproduce edge-case it is much more difficult to do). What also seems to have helped a bit is not defining anonymous callback functions for the div.
import React, { createRef } from "react";
export default class Geo extends React.Component {
state = {
isHovering: false
};
mouseMoveRef = createRef();
ponentDidMount() {
window.addEventListener("mousemove", this.checkHover, true);
}
ponentWillUnmount() {
window.removeEventListener("mousemove", this.checkHover, true);
}
setHover = () => this.setState({ isHovering: true });
setUnhover = () => this.setState({ isHovering: false });
checkHover = e => {
if (this.mouseMoveRef.current) {
const { isHovering } = this.state;
const mouseOver = this.mouseMoveRef.current.contains(e.target);
if (!isHovering && mouseOver) {
this.setHover();
}
if (isHovering && !mouseOver) {
this.setUnhover();
}
}
};
render() {
var textDisplay;
if (this.state.isHovering) {
textDisplay = <span>HOVERING</span>;
} else {
textDisplay = <h1>NOT HOVERING</h1>;
}
return (
<div
ref={this.mouseMoveRef}
onMouseEnter={this.setHover}
onMouseLeave={this.setUnhover}
style={{ width: 300, height: 100, background: "green" }}
>
{textDisplay}
</div>
);
}
}
As far as I can see, you have a problem with the way you update the state. Bear in mind that React may update the state asynchronously.
Changing toggleHoverState function will solve the issue
toggleHoverState() {
this.setState(state => ({isHovering: !state.isHovering}));
}
Go to this section in React docs for more info