How can I prevent event bubbling in nested React components on click?
I had the same issue. I found stopPropagation did work. I would split the list item into a separate component, as so:
class List extends React.Component {
handleClick = e => {
// do something
}
render() {
return (
<ul onClick={this.handleClick}>
<ListItem onClick={this.handleClick}>Item</ListItem>
</ul>
)
}
}
class ListItem extends React.Component {
handleClick = e => {
e.stopPropagation(); // <------ Here is the magic
this.props.onClick();
}
render() {
return (
<li onClick={this.handleClick}>
{this.props.children}
</li>
)
}
}
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.
stopPropagation: function(e){
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}
On the order of DOM events: CAPTURING vs BUBBLING
There are two stages for how events propagate. These are called "capturing" and "bubbling".
| | / \
---------------| |----------------- ---------------| |-----------------
| element1 | | | | element1 | | |
| -----------| |----------- | | -----------| |----------- |
| |element2 \ / | | | |element2 | | | |
| ------------------------- | | ------------------------- |
| Event CAPTURING | | Event BUBBLING |
----------------------------------- -----------------------------------
The capturing stage happen first, and are then followed by the bubbling stage. When you register an event using the regular DOM api, the events will be part of the bubbling stage by default, but this can be specified upon event creation
// CAPTURING event
button.addEventListener('click', handleClick, true)
// BUBBLING events
button.addEventListener('click', handleClick, false)
button.addEventListener('click', handleClick)
In React, bubbling events are also what you use by default.
// handleClick is a BUBBLING (synthetic) event
<button onClick={handleClick}></button>
// handleClick is a CAPTURING (synthetic) event
<button onClickCapture={handleClick}></button>
Let's take a look inside our handleClick callback (React):
function handleClick(e) {
// This will prevent any synthetic events from firing after this one
e.stopPropagation()
}
function handleClick(e) {
// This will set e.defaultPrevented to true
// (for all synthetic events firing after this one)
e.preventDefault()
}
An alternative that I haven't seen mentioned here
If you call e.preventDefault() in all of your events, you can check if an event has already been handled, and prevent it from being handled again:
handleEvent(e) {
if (e.defaultPrevented) return // Exits here if event has been handled
e.preventDefault()
// Perform whatever you need to here.
}
For the difference between synthetic events and native events, see the React documentation: https://reactjs.org/docs/events.html
This is an easy way to prevent the click event from moving forward to the next component and then call your yourFunction.
<Button onClick={(e)=> {e.stopPropagation(); yourFunction(someParam)}}>Delete</Button>
This is not 100% ideal, but if it is either too much of a pain to pass down props
in children -> children fashion or create a Context.Provider
/Context.Consumer
just for this purpose), and you are dealing with another library which has it's own handler it runs before yours, you can also try:
function myHandler(e) {
e.persist();
e.nativeEvent.stopImmediatePropagation();
e.stopPropagation();
}
From what I understand, the event.persist
method prevents an object from immediately being thrown back into React's SyntheticEvent
pool. So because of that, the event
passed in React actually doesn't exist by the time you reach for it! This happens in grandchildren because of the way React handle's things internally by first checking parent on down for SyntheticEvent
handlers (especially if the parent had a callback).
As long as you are not calling persist
on something which would create significant memory to keep creating events such as onMouseMove
(and you are not creating some kind of Cookie Clicker game like Grandma's Cookies), it should be perfectly fine!
Also note: from reading around their GitHub occasionally, we should keep our eyes out for future versions of React as they may eventually resolve some of the pain with this as they seem to be going towards making folding React code in a compiler/transpiler.