1

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 become 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.

Evan Hessler
  • 267
  • 3
  • 14
  • 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 component and not the overall structure of what's rendered. – Drew Reese Aug 03 '20 at 00:47
  • @DrewReese Yes, I can. Please see https://codesandbox.io/s/aged-butterfly-r2g6x?file=/src/Geo.js. I will add it to the post. – Evan Hessler Aug 03 '20 at 02:31

2 Answers2

4

Issue

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:

  1. 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.
  2. The component 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();

  componentDidMount() {
    window.addEventListener("mousemove", this.checkHover, true);
  }

  componentWillUnmount() {
    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>
    );
  }
}

Edit friendly-chatterjee-xx2ms

Drew Reese
  • 103,803
  • 12
  • 69
  • 96
0

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

Nahuel
  • 121
  • 2
  • Unfortunately that didn't do it. I reproduced the issue here: https://codesandbox.io/s/aged-butterfly-r2g6x?file=/src/Geo.js – Evan Hessler Aug 03 '20 at 02:32
  • Well, in this case you know that when the mouse leaves you have to hide the component (set isHovering to false) and that when it hovers over the component you have to set the flag back to true. Then you can just do: ``` onMouseEnter={() => this.setState({ isHovering: true })} onMouseLeave={() => this.setState({ isHovering: false })} ``` instead of depending on the previous state – Nahuel Aug 03 '20 at 04:37
  • That didn't seem to have any effect. See https://codesandbox.io/s/infallible-napier-lmt69?file=/src/Geo.js – Evan Hessler Aug 03 '20 at 18:17
  • Is there any known issue of onMouseEnter and onMouseLeave not triggering properly? – Evan Hessler Aug 03 '20 at 18:58