June 6, 2020
6 min read
I was stuck on a problem for a few days on a side project in React that had me questioning my understanding of how event propagation works in JavaScript. This article by Gideon Pyzer saved my sanity. I'm going to summarize my understanding of this issue here so hopefully I can find this quicker in the future!
Plenty of websites explain how event bubbling (propagation) works in the browser better than I can, so I won't go into detail here.
To greatly summarize, a DOM node will get the chance to respond to an event before its parents do.
Take the example HTML document and JavaScript code below.
<div> <button>Click me to trigger a "click" event.</button></div>
const div = document.querySelector("div");const button = document.querySelector("button");
div.addEventListener("click", (e) => console.log("Hello from div"));button.addEventListener("click", (e) => console.log("Hello from button"));
Because the button gets to respond to the event first, the console will print out in this order:
Hello from button
Hello from div
Sometimes, you don't want a parent node to also respond to an event. JavaScript events have a stopPropagation function on them to prevent any nodes further along in the bubble chain from receiving the event.
Let's update the button event handler in the JavaScript code from above to the following:
button.addEventListener("click", (e) => { e.stopPropagation(); console.log("Hello from button");});
The button event handler now prevents the event from propagating up to the body event listener so our console output looks like this:
Hello from button
Recently, I was creating a React app where I wanted to listen to global click
events. To accomplish this, I was listening for click events on the body when my App mounted. Additionally, I wanted buttons to be able to override the default global behavior. Below is a sample React app I thought would accomplish these things.
const App = () => { useEffect(() => { // attach a click event listener to the body of the HTML document when App "mounts" document.body.addEventListener("click", (e) => console.log("Hello from body, default behavior") ); }, []);
return ( <div> <button onClick={(e) => { e.stopPropagation(); console.log("Hello from button, special behavior"); }} > Click me to trigger a "click" event. </button> </div> );};
The App mounts a body event listener with my default global behaviour. My button stops event propagation to allow it to trigger unique behavior and avoid the default behavior. I thought this would behave in the same way as the traditional DOM events in the previous sections. However, when I clicked the button I saw the following unexpected response:
Hello from div, default behavior
Hello from button, special behavior
My call to stopPropagation
didn't seem to be working! Both behaviors were still being triggered, and weirdly they weren't even being triggered in the order that I expected them to be occuring. After much googling and experimenting I learned why this had happened once I found Gideon Pyzer's blog post.
React doesn't use the same DOM events that the browser uses. Instead it uses something called Synthetic Events that have a similar API to native DOM events. These events have optimizations and allow for a cross compatability layer for use with different React renderers like React Native.
Because these Synthetic Events are a React construct, the onClick
handler I set on the button
component above is not a true DOM event like I originally assumed, it's a synthetic event. React doesn't guarantee that stopping propagation in a synthetic event will prevent native DOM events from propagating.
There's not a default way to actually listen to all click events in the body via React. Instead, you can attach an onClick
listener to the root element in your React component tree. When clicking on the button, the below code will log out as we expected it to earlier.
const App = () => { return ( <div onClick={(e) => console.log("Hello from body, default behavior")}> <button onClick={(e) => { e.stopPropagation(); console.log("Hello from button, special behavior"); }} > Click me to trigger a "click" event. </button> </div> );};
Hello from button, special behavior
Based on this information, my recommendation is not to use DOM events and synthetic events that rely on each other. A safer rule of thumb is probably:
Always use React's synthetic events instead of native DOM events.
There may be scenarios where it makes sense to mix DOM events in. But generally, React's role is to prevent you from needing to interact directly with the DOM. If you find yourself doing so, there's a chance you might run into some unexpected behavior where React cannot correctly optimize itself for you.