151

I'm trying to use event.stopPropagation() within a ReactJS component to stop a click event from bubbling up and triggering a click event that was attached with JQuery in legacy code, but it seems like React's stopPropagation() only stops propagation to events also attached in React, and JQuery's stopPropagation() doesn't stop propagation to events attached with React.

Is there any way to make stopPropagation() work across these events? I wrote a simple JSFiddle to demonstrate these behaviors:

/** @jsx React.DOM */
var Propagation = React.createClass({
    alert: function(){
        alert('React Alert');
    },
    stopPropagation: function(e){
        e.stopPropagation();
    },
    render: function(){
        return (
            <div>
                <div onClick={this.alert}>
                    <a href="#" onClick={this.stopPropagation}>React Stop Propagation on React Event</a>
                </div>
                <div className="alert">
                    <a href="#" onClick={this.stopPropagation}>React Stop Propagation on JQuery Event</a>
                </div>
                <div onClick={this.alert}>
                    <a href="#" className="stop-propagation">JQuery Stop Propagation on React Event</a>
                </div>
                <div className="alert">
                    <a href="#" className="stop-propagation">JQuery Stop Propagation on JQuery Event</a>
                </div>
            </div>
        );
    }
});

React.renderComponent(<Propagation />, document.body);

$(function(){    
    $(document).on('click', '.alert', function(e){
        alert('Jquery Alert');
    });

    $(document).on('click', '.stop-propagation', function(e){
        e.stopPropagation();
    });
});
Edgar
  • 6,022
  • 8
  • 33
  • 66
lofiinterstate
  • 1,513
  • 2
  • 10
  • 4
  • 12
    React's actual event listener is also at the root of the document, meaning the click event has already bubbled to the root. You can use `event.nativeEvent.stopImmediatePropagation` to prevent other event listeners from firing, but order of execution is not guaranteed. – Ross Allen Jun 25 '14 at 18:34
  • 3
    Actually, [`stopImmediatePropagation`](https://developer.mozilla.org/en-US/docs/Web/API/event.stopImmediatePropagation) claims event listeners will be called in the order in which they were bound. If your React JS is initialized before your jQuery (as it is in your fiddle), stopping immediate propagation will work. – Ross Allen Jun 25 '14 at 18:38
  • 1
    React stopping jQuery listeners from being called: http://jsfiddle.net/7LEDT/5/ It's not possible for jQuery to prevent React from being called because jQuery listeners are bound later in your fiddle. – Ross Allen Jun 25 '14 at 18:40
  • One option would be to set up your own jQuery event handler in `componentDidMount`, but it might interfere with other React event handlers in unexpected ways. – Douglas Jun 25 '14 at 21:53
  • I realized the second example on `.stop-propagation` necessarily will not work. Your example uses event delegation but is trying to stop propagation at the element. The listener needs to be bound to the element itself: `$('.stop-propagation').on('click', function(e) { e.stopPropagation(); });`. This fiddle prevents all propagation like you were trying: http://jsfiddle.net/7LEDT/6/ – Ross Allen Jun 26 '14 at 03:07
  • I had similar problem and solved it here http://stackoverflow.com/q/25862475/679340 – Piotr Perak Sep 16 '14 at 21:25
  • https://gist.github.com/xgqfrms-GitHub/e59f6eca03da7c1960013cc94e04e84f – xgqfrms Jun 30 '17 at 14:31

11 Answers11

190

React uses event delegation with a single event listener on document for events that bubble, like 'click' in this example, which means stopping propagation is not possible; the real event has already propagated by the time you interact with it in React. stopPropagation on React's synthetic event is possible because React handles propagation of synthetic events internally.

Working JSFiddle with the fixes from below.

React Stop Propagation on jQuery Event

Use Event.stopImmediatePropagation to prevent your other (jQuery in this case) listeners on the root from being called. It is supported in IE9+ and modern browsers.

stopPropagation: function(e){
    e.stopPropagation();
    e.nativeEvent.stopImmediatePropagation();
},
  • Caveat: Listeners are called in the order in which they are bound. React must be initialized before other code (jQuery here) for this to work.

jQuery Stop Propagation on React Event

Your jQuery code uses event delegation as well, which means calling stopPropagation in the handler is not stopping anything; the event has already propagated to document, and React's listener will be triggered.

// Listener bound to `document`, event delegation
$(document).on('click', '.stop-propagation', function(e){
    e.stopPropagation();
});

To prevent propagation beyond the element, the listener must be bound to the element itself:

// Listener bound to `.stop-propagation`, no delegation
$('.stop-propagation').on('click', function(e){
    e.stopPropagation();
});

Edit (2016/01/14): Clarified that delegation is necessarily only used for events that bubble. For more details on event handling, React's source has descriptive comments: ReactBrowserEventEmitter.js.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Ross Allen
  • 43,772
  • 14
  • 97
  • 95
  • @ssorellen: Clarification: does React bind its event listener on `document`, or the root React component? The [linked page](https://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#under-the-hood-autobinding-and-event-delegation) just says "at the top level", which I take to mean that it *not* the `document` (but I can't figure out if this is even relevant). – user5670895 Jan 14 '16 at 15:33
  • 2
    @FighterJet That depends on the event type. For events that bubble, top-level delegation is used. For events that don't bubble, React necessarily listens on various DOM nodes. A more thorough description is included in ReactBrowserEventEmitter.js: https://github.com/facebook/react/blob/3b96650e39ddda5ba49245713ef16dbc52d25e9e/src/renderers/dom/client/ReactBrowserEventEmitter.js#L23 – Ross Allen Jan 14 '16 at 19:20
  • Also check out Jim's answer, which suggests adding the global click listener to `window` instead of `document`: https://stackoverflow.com/a/52879137/438273 – jsejcksn Jan 01 '20 at 05:07
23

It is still one intersting moment:

ev.preventDefault()
ev.stopPropagation();
ev.nativeEvent.stopImmediatePropagation();

Use this construction, if your function is wrapped by tag

Roman
  • 245
  • 2
  • 4
20

Worth noting (from this issue) that if you're attaching events to document, e.stopPropagation() isn't going to help. As a workaround, you can use window.addEventListener() instead of document.addEventListener, then event.stopPropagation() will stop event from propagating to the window.

Jim Nielsen
  • 213
  • 1
  • 3
  • 7
9

From the React documentation: The event handlers below are triggered by an event in the bubbling phase. To register an event handler for the capture phase, append Capture. (emphasis added)

If you have a click event listener in your React code and you don't want it to bubble up, I think what you want to do is use onClickCapture instead of onClick. Then you would pass the event to the handler and do event.nativeEvent.stopPropagation() to keep the native event from bubbling up to a vanilla JS event listener (or anything that's not react).

Neil Girardi
  • 4,533
  • 1
  • 28
  • 45
  • 3
    Thank you, that's what worked for me as of 2021 and React 17. onClickCapture={e => e.stopPropagation()} was enough though, didn't have to use event.nativeEvent to stop propagation to the root. – Hans May 09 '21 at 06:25
7

I was able to resolve this by adding the following to my component:

componentDidMount() {
  ReactDOM.findDOMNode(this).addEventListener('click', (event) => {
    event.stopPropagation();
  }, false);
}
senornestor
  • 4,075
  • 2
  • 33
  • 33
  • 2
    This will stop SynteticEvents altogether, because React uses event delegation with a single event listener on document for events that bubble. – Vakhtang Aug 24 '17 at 20:10
6

React 17 delegates events to root instead of document, which might solve the problem. More details here. You can also refer to my blog.

Chetan Gawai
  • 2,361
  • 1
  • 25
  • 36
  • 1
    Some months ago I had a listener on document that would still receive a mousedown event even after `event.stopPropagation()` was called (and I relied on this behavior), but noticed that it stopped getting called after I updated a lot of libraries. I suspected the cause was the update of ReactJS, and your answer now confirms/explains it. – Venryx Aug 02 '21 at 11:26
4

I ran into this problem yesterday, so I created a React-friendly solution.

Check out react-native-listener. It's working very well so far. Feedback appreciated.

Erik R.
  • 7,152
  • 1
  • 29
  • 39
  • 5
    `react-native-listener` uses `findDomNode` which will be deprecated https://github.com/yannickcr/eslint-plugin-react/issues/678#issue-165177220 – Bohdan Lyzanets Nov 30 '16 at 14:07
  • 1
    Downvoted because answers are not an appropriate place to market or deliver external solutions which do not complement a complete answer. – jimmont Dec 10 '18 at 21:56
4

A quick workaround is using window.addEventListener instead of document.addEventListener.

Yuki
  • 3,857
  • 5
  • 25
  • 43
2

Update: You can now <Elem onClick={ proxy => proxy.stopPropagation() } />

Eric Hodonsky
  • 5,617
  • 4
  • 26
  • 36
  • 1
    See @RossAllen's answer. This will not prevent propagation to an ancestor's native DOM listeners (which jQuery uses). – Ben Mosher Nov 15 '17 at 21:56
  • Well this is the right way if you're using a react component. Highjacking the event isn't a good idea. – Eric Hodonsky Nov 16 '17 at 23:27
  • If something else is failing, it's because you're doing something else wrong – Eric Hodonsky Nov 16 '17 at 23:27
  • 1
    I agree that that is the correct way if all your listeners are attached via React, but that is not the question here. – Ben Mosher Nov 17 '17 at 12:27
  • That's just me... I will always encourage correct use of the correct way instead of leading somebody astray with a work around that may cause them grief in the future because they have a 'solution' to a problem. – Eric Hodonsky Nov 17 '17 at 20:39
1

In my case e.stopPropagation() didn't work because child had onChange event, parent onClick. Got insight from another StackOverflow answer

The change and click events are different events.

Meagning: e.stopPropagation() calling inside onChange won't prevent firing onClick.

Solution to have both onChange or onClick.

Jasurbek Nabijonov
  • 1,607
  • 3
  • 24
  • 37
1

The way i've solved this is by adding an if statement at the callback that check the event.target and if its diferent to the element i expect, then return from the function

// Callback from my own app
function exitResultsScreen(event){
        // Check element by ID
        if (event.target.className !== sass.div__search_screen){return} 
        
       // Executed only when the right elements calls
        setShowResults(false)
}
X71
  • 23
  • 5