Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React and jQuery event handler's fired in wrong order

I'm creating menu that appears at the top of the screen. When user clicks one of menu items div with a lot of links appear. I wan't to be able to hide this div by clicking another menu item (no problem, already implemented it) but also by clicking anywhere else then this div and menuitems.

I heard about two solutions:

  • Show invisible div that is behind the menu and covers whole screen and add click handler to it.
  • Attach event handler to document.body.

First one is not good for me because that div will cover links that are on the page and I want them to be clickable even after menuitem is clicked and it's corresponding div appeared. So I tried second soultion. But the problem is that jquery click handler on body is fired before my component's handler. I have no idea how to make it first call my components handler and then prevent event propagation.

Here is the code and js fiddle:

/** @jsx React.DOM */
var Menu = React.createClass({
    click: function(e) {
        console.log('component handled click - should be called before jquery one and prevent jquery handler from running at all');
        e.stopPropagation();
    },
    render: function(){
    console.log("component renders");
        return (
            <div>
                <div>| MenuItem1 | MenuItem2 | MenuItem 3 |</div>
                <br />
                <div className="drop">
                    MenuItem 1 (This div appears when you click menu item)
                    <ul>
                        <li><a href="#" onClick={this.click}>Item 1 - when I click this I don't want document.body.click to fire</a></li>
                        <li><a href="#" onClick={this.click}>Item 1 - when I click this I don't want document.body.click to fire</a></li>
                    </ul>
                </div>
            </div>
        );
    }
});

React.renderComponent(<Menu />, document.getElementById("menu"));

$(document).ready(function() {
    console.log('document is ready');
    $("body").click(function() {
        console.log('jquery handled click - should not happen before component handled click');
    });
});

http://jsfiddle.net/psonsx6j/

startup console before you run it for explanation of problem

like image 953
Piotr Perak Avatar asked Sep 16 '14 07:09

Piotr Perak


People also ask

How may one stop the propagation of react synthetic event up by the DOM tree?

As a workaround, you can use window. addEventListener() instead of document. addEventListener , then event. stopPropagation() will stop event from propagating to the window.

Which event can be used to stop event propagation in react?

stopPropagation() The stopPropagation() method of the Event interface prevents further propagation of the current event in the capturing and bubbling phases.


2 Answers

So I solved it. When you add event handler to document using jQuery this event handler is fired first. So you can't catch event and stop it's propagation. But when you attach handler with document.addEventlistener this handler is being called last and you have opportunity to react :) to click in React components.

I modified Mike's code to make it work (http://jsfiddle.net/s8hmstzz/):

/** @jsx React.DOM */
var Menu = React.createClass({
    getInitialState: function() {
        return {
            show: true
        }
    },
    componentDidMount: function() {
        // when attaching with jquery this event handler is run before other handlers
        //$('body').bind('click', this.bodyClickHandler);
        document.addEventListener('click', this.bodyClickHandler);
    },
    componentWillUnmount: function() {
        //$('body').unbind('click', this.bodyClickHandler);
        document.removeEventListener('click', this.bodyClickHandler);
    },
    anchorClickHandler: function(e) {
        console.log('click on anchor - doing some stuff and then stopping propation');
        e.stopPropagation();
        e.nativeEvent.stopImmediatePropagation();
    },
    bodyClickHandler: function(e) {
        console.log('bodyclickhandler');
        this.setState({show: false})
    },
    clickHandler: function(e) {
        // react to click on menu item
    },
    preventEventBubbling: function(e) {
        console.log('click on div - stopping propagation');
        e.stopPropagation();
        e.nativeEvent.stopImmediatePropagation();
    },
    render: function() {
        return (
            <div>
                <a href="#" onClick={this.anchorClickHandler}>Link - will stop propagation</a>
                <div id="menudrop" onClick={this.preventEventBubbling} style={{display: this.state.show ? 'block' : 'none'}}>
                    <ul>
                        <li onClick={this.clickHandler}>Menu Item</li>
                    </ul>
                </div>
            </div>
        )
    }
});

React.renderComponent(<Menu />, document.getElementById('menu'));

Start console and then click div, anchor and body to see how it works. I don't know why using addEventListener instead of jQuery changes order of event handlers fired.

like image 104
Piotr Perak Avatar answered Oct 22 '22 22:10

Piotr Perak


See my comment above.

Basically you can't rely on being able to stop propagation of an event like onClick in React when using jquery (or normal JS events) in conjunction.

What you can do is check in your $('body').click() function that you're not clicking on the menu (so it doesn't close your menu after clicking an item in it). Also it's worth noting that you should be binding this body event INSIDE the react component so that you can cleanly add/remove it just by mounting the component and not have the component's functionality be significantly altered by something that the component itself does not know anything about.

So what you do is something like this (nb: untested code but based on a component that I have with this same issue)

var Menu = React.createClass({
    getInitialState: function() {
        return {
            show: true
        }
    },
    componentDidMount: function() {
        $('body').bind('click', this.bodyClickHandler);
    },
    componentWillUnmount: function() {
        $('body').unbind('click', this.bodyClickHandler);
    },
    bodyClickHandler: function(e) {
        if ($(e.target).parents('div').get(0) !== this.getDOMNode()) {
            this.setState({
                show: false
            })
        }
    },
    clickHandler: function(e) {
        // react to click on menu item
    },
    render: function() {
        return (
            <div style={{display: this.state.show ? 'block' : 'none'}}>
                <ul>
                    <li onClick={this.clickHandler}>Menu Item</li>
                </ul>
            </div>
        )
    }
});

So in the above code, we assume you want to show the menu dropdown as soon as the component is mounted to the dom. This may not be the case as you probably want to mount the component and then show/hide it based on hover. In which case you'd just need to move the $('body').bind() event binding from componentDidMount to a callback wherever you are showing the menu. $('body').unbind() should probably be moved to whenever the component is hidden. You then end up with a perfect situation where when the menu is shown, we bind a body event waiting for a click, and unbind it whenever it's hidden, and/or unmounted.

What occurs is that the bodyClickHandler is called before clickHandler when clicking on the menu, however bodyClickHandler only hides the menu if where you clicked's parent tree does not contain the root div of our component (this.getDOMNode() gives the html element of the root node inside render()'s return).

As a result clicking outside the component will close it, clicking inside the component will do nothing, clicking on the <li> will fire clickHandler. All working as expected.

Hope this helps.

like image 35
Mike Driver Avatar answered Oct 22 '22 23:10

Mike Driver