New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

onMouseLeave doesn't work if the node gets detached #6807

Open
ghost opened this Issue May 19, 2016 · 13 comments

Comments

Projects
None yet
8 participants
@ghost
Copy link

ghost commented May 19, 2016

I have a problem with this kind of component:

class onHover extends Component {

  constructor(props) {
    super(props);
    this.state = {
      bool: false,
    }
  }

  render() {
    return (
      <div onMouseEnter={() => this.setState({ bool: true })} onMouseLeave={() => this.setState({ bool: false })}>
        {
          this.state.bool ? (
            <span>[OPTION1] show after onMouseEnter</span>
          ) : (
            <div>[OPTION2] show after onMouseLeave</div>
          )
        }
      </div>
    )
  }
}

Notice that the first option1 is a span, option2 is a div.

This works fine when I move the mouse slowly.
Though, if I "cut" through this with the mouse very fast, only the onMouseEnter event gets triggered, but not the onMouseLeave event.

It is always working though, if both options have the same tag (if both are div or both are span).

EDIT:
I think it has something to do with rerendering. When the components are of the same type, but I force a rerender, it causes the same issues.

class onHover extends Component {

  constructor(props) {
    super(props);
    this.state = {
      bool: false,
    }
  }

  render() {
    return (
      <div onMouseEnter={() => this.setState({ bool: true })} onMouseLeave={() => this.setState({ bool: false })}>
        {
          this.state.bool ? (
            <div key={Math.random()}>[OPTION1] show after onMouseEnter</div>
          ) : (
            <div key={Math.random()}>[OPTION2] show after onMouseLeave</div>
          )
        }
      </div>
    )
  }
}

@ghost ghost changed the title [React 14.7] mouseOnLeave not working after rerender [React 14.7] mouseOnLeave not working May 19, 2016

@ghost ghost changed the title [React 14.7] mouseOnLeave not working [React 14.7] mouseOnLeave not working as expected May 19, 2016

@syranide

This comment has been minimized.

Copy link
Contributor

syranide commented May 20, 2016

#4492 related I think.

Anyway, it's simply because the element emitting the event is replaced and it messes up the events. I could be wrong, but IIRC there's also some weird browser/spec thing involved in this behavior too (just saying).

@rishirajsurti

This comment has been minimized.

Copy link

rishirajsurti commented Jul 12, 2016

I am new here. What I am thinking is that, can we create a new variable which stores on which tag is the mouse over, currently?
As with #4492, the mouse cannot be over two elements. If we can detect if currently the mouse is over some other element, then it should automatically trigger the onMouseOut event in rest other elements, if the onMouseOver is true.
Also, can we bring in another variable involving the mouse speed? Any thoughts?

@gaearon gaearon changed the title [React 14.7] mouseOnLeave not working as expected onMouseLeave not working as expected Oct 4, 2017

@gaearon gaearon referenced this issue Oct 4, 2017

Open

Umbrella: React DOM Bugs #11097

11 of 29 tasks complete
@gwildu

This comment has been minimized.

Copy link

gwildu commented Feb 20, 2018

Any news on this issue? As far as I see, this makes it almost impossible to write performant code with requestAnimationFrame or throttling for mouse move events. So it seems quite relevant.

@gaearon

This comment has been minimized.

Copy link
Member

gaearon commented Apr 17, 2018

What kind of news are you expecting? The browsers aren’t consistent about firing events on deleted elements. I don’t think it’s something we can fix in React, but happy to hear suggestions.

For now I’ll close as I don’t see this issue is actionable for us. If you want to avoid such problems, don’t remove/replace the hovered elements during hover.

@gaearon gaearon closed this Apr 17, 2018

@gaearon gaearon reopened this Apr 18, 2018

@gaearon gaearon changed the title onMouseLeave not working as expected onMouseLeave doesn't work if the node gets detached Apr 18, 2018

@gaearon

This comment has been minimized.

Copy link
Member

gaearon commented Apr 18, 2018

Reopening per conversation with @sophiebits who pointed out we might be able to fix it by not relying on bubbling.

@casvil

This comment has been minimized.

Copy link

casvil commented Oct 2, 2018

Having same issue.

@gaearon

If you want to avoid such problems, don’t remove/replace the hovered elements during hover.

How is this achievable? I am using render props and render conditionally one or another depending on hover state.

Thanks!

@ryansaam

This comment has been minimized.

Copy link

ryansaam commented Oct 2, 2018

Couldn't you write something like so:

class onHover extends Component {

  constructor(props) {
    super(props);
    this.state = {
      bool: false,
    }
    this.handleMouse = this.handleMouse.bind(this)
  }
  handleMouse() {
    this.setState((state) => ({ bool: !state.bool }))
  }
  render() {
    return (
      <div onMouseEnter={this.handleMouse} onMouseLeave={this.handleMouse}>
        <div style={{display: this.state.bool ? "block" : "none"}} key={Math.random()}>[OPTION1] show after onMouseEnter</div>
        <div style={{display: this.state.bool ? "none" : "block"}} key={Math.random()}>[OPTION2] show after onMouseLeave</div>
      </div>
    )
  }
}

It looks a little bit cleaner in the return statement and I'm pretty sure you'll get the same effect you're looking for. Also try reading this section of React docs Conditional Rendering

@casvil

This comment has been minimized.

Copy link

casvil commented Oct 2, 2018

Your idea looks cool but it's using the same onMouseLeave as I was and I assume it would still fail on a fast mouse movement, so the bool parameter inside this.state would remain true and the mouse would not be hovering the element.

I solved it by using styled-components.

@carpben

This comment has been minimized.

Copy link

carpben commented Nov 22, 2018

I think I might be experiencing a similar problem, but the other way around; onMouseLeave is triggered for no reason. I build a simple image carousel. When the mouse hovers over the image an onMouseEnter event is triggered, and a front overlay is rendered with controls to move to the next or previous image. When clicking the arrow to move forward or backwards every few clicks an onMouseLeave is triggered for no apparent reason.

Here is a link to a gist and screenshots.

image

https://gist.github.com/carpben/364e7d6c34cd0f9fa9e5ddc10181c23b

Not sure why this is happening, and what are possible solutions.
Could I tell somehow if the node is destroyed and a new one is build?

@casvil

This comment has been minimized.

Copy link

casvil commented Nov 22, 2018

Could I tell somehow if the node is destroyed and a new one is build?

I guess you can log a message inside componentWillUnmount and see if it gets destroyed

@carpben

This comment has been minimized.

Copy link

carpben commented Nov 22, 2018

@casvil The component in the gist isn't destroyed, I suspect it's the child styled-component that is destroyed.

@carpben

This comment has been minimized.

Copy link

carpben commented Nov 25, 2018

Sorry, my issue does not belong here, and is related to this bug: https://stackoverflow.com/questions/45266854/mouseleave-triggered-by-click

@mrdanimal

This comment has been minimized.

Copy link

mrdanimal commented Dec 10, 2018

Here's how I (almost) solved it, although I occasionally get an error "Unable to find node on an unmounted component". It's very very rare though and only seems to happen sometimes if the component is rendered when the mouse is moving over it.

...
  componentDidMount() {
    this._throttledHandleMouseMove = throttle(this.handleMouseMove, 200);
  }

  componentWillUnmount() {
    this.removeMouseMoveHandler();
  }

  addMouseMoveHandler = () => {
    addWindowEventListener('mousemove', this._throttledHandleMouseMove);
  };

  removeMouseMoveHandler = () => {
    removeWindowEventListener('mousemove', this._throttledHandleMouseMove);
  };

  handleMouseMove = (e) => {
    const rect = ReactDOM.findDOMNode(this).getBoundingClientRect(); // very rarely, this throws an error

    if (
      e.clientX < rect.left
      || e.clientX > rect.right
      || e.clientY < rect.top
      || e.clientY > rect.bottom
    ) {
      this.setState({ isRowHovered: false });
      this.removeMouseMoveHandler();
    }
  };

  handleMouseEnter = () => {
    this.setState({ isHovered: true });
    this.addMouseMoveHandler();
  };

  handleMouseLeave = () => {
    this.setState({ isHovered: false });
    this.removeMouseMoveHandler();
  };

  render() {
    return (
      <div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
        My Component
      </div>
    );
  }

...

This has been good enough for me, but if anyone has a thought as to how ReactDOM.findDOMNode(this) can fail (in order for it to be called, I assume the component must be mounted), I'd love to know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment