Written by Isaac Okoro✏️
Events are detectable actions or occurrences commonly used in programs to trigger functions. These actions include a user clicking a button and a webpage loading.
This article will cover how event capturing and bubbling work in React. Before we go over what event capturing and bubbling are, we’ll learn about event delegation and how it works. We will also look at the SyntheticEvent
in React, how it works, and its connection to event capturing and bubbling in React.
Jump ahead:
- Understanding event delegation in React
- Intro to event propagation
- What is the
SyntheticEvent
? - Using
event.stopPropagation()
in React - Event delegation in React v16 vs. React v17
Understanding event delegation in React
Event delegation is a technique for handling events that involves adding a single event listener to a parent element rather than adding event listeners to individual child elements. When an event occurs on a child element, the event listener on the parent element is notified, and the parent element can handle the event. This is useful when you have many child elements and want to avoid adding event listeners to them.
Imagine a scenario in which you have the following code:
function App() {
const parentFunction = () => {
console.log('Hello from the parent');
};
const firstChildFunction = () => {
console.log('Hello from the first child');
};
const secondChildFunction = () => {
console.log('Hello from the second child');
};
const thirdChildFunction = () => {
console.log('Hello from the third child');
};
return (
<div className="App">
<div className="parent-div" onClick={parentFunction}>
<button className="first-child-button" onClick={firstChildFunction}>
First child button
</button>
<button className="second-child-button" onClick={secondChildFunction}>
Second child button
</button>
<button className="third-child-button" onClick={thirdChildFunction}>
Third child button
</button>
</div>
</div>
);
}
export default App;
In the code above, we have a parent div
with an onClick
handler attached to it. The parent div
houses three children buttons
with onClick
handlers attached to them. The onClick
handles all the PointTo
functions that print text to the console.
When we click any of the buttons and check the console, we get the log for the button, which is the second button. However, we also get another log that shows that the parent div
was clicked, as shown in the image below:
We know that event delegation took place because the parent element also handles the event of the children element. With that out of the way, let’s look at the order in which React handles the event.
Intro to event propagation
Event propagation is the order in which event handlers are called when an event occurs on a webpage. To understand how event propagation works, it helps to know about the three phases of event flow in the DOM: the capturing phase, the target phase, and the bubbling phase.
In the capturing phase, the event propagates from the top of the document down to the element on which the event occurred. Capturing is less commonly used than bubbling. Simply put, this is otherwise known as an event moving downwards from the top of the document to the element on which the said event occurred.
In the target phase, the event handler is executed on the element on which the event occurred and can be assessed via event.target
.
In the bubbling phase, the event moves back up the DOM tree from the element where the event occurred to its parent element, and so on, until it reaches the top of the document. This is the default behavior for most events in the DOM. This is the reverse of the capturing phase.
The image below describes the three phases of event propagation and how it works:
Note: There is a fourth phase of event propagation known as the none phase, where no event is executed.
What is the SyntheticEvent
?
React relies on a virtual DOM and uses this concept to handle DOM operations. The SyntheticEvent
wrapper wraps around the native DOM event that React uses to abstract the differences between the events the browser can handle. This allows you to write event handlers that work consistently across all browsers.
For example, consider the following code that attaches a click EventListener
to a button
:
document.getElementById('myButton').addEventListener('click', () => {
console.log('Button was clicked');
});
In the code above, the click
event is a native DOM event. In React, you would write that event like so:
<button onClick={() => console.log('Button was clicked')}>Click me</button>
The onClick
event in this example is a synthetic event. React will call the event handler function you provided when the button is clicked. Internally, React will convert the synthetic event into a native DOM event and pass it to the event listener.
React creates SyntheticEvents
to allow you to write your event handlers in a way familiar to you without worrying about the differences between the various events the browser can handle. SyntheticEvents
also include methods such as stopPropagation
and preventDefault
, which are instrumental to React's Event System.
Using event.stopPropagation()
in React
We have looked at event propagation, its phases, and how they work. However, there are occasions when you don't want an event to bubble. This is where the event.stopPropagation()
method comes in.
In React, the event.stopPropagation()
method prevents an event from being handled by the parent component if a child component handles it. You can call event.stopPropagation()
on the event object to stop the event from propagating further up the tree. This is useful in cases where the parent component has an event handler that conflicts with the event handler of the child component.
Let's see an example of a use case where you would want to use event.stopPropagation()
to avoid conflict between the parent and child components. In the code below, we have a header
component that has a button
for logging out:
import './App.css';
function App() {
const handleHeaderClick = () => {
console.log('Header was clicked');
};
const handleButtonClick = () => {
console.log('The logout button was clicked');
};
return (
<>
<div className="styleHeader" onClick={handleHeaderClick}>
<div>Header</div>
<button type="button" onClick={handleButtonClick}>
Log Out
</button>
</div>
</>
);
}
export default App;
The code block above will display the header
component as shown in the image below:
In such cases, we don't want the event handled by the parent component. At this point, if you click the logout button, the parent still handles the event as shown below:
We can stop the parent component from handling the event by passing the event.stopPropagation()
to the child component — the button — as shown below:
import './App.css';
function App() {
const handleHeaderClick = () => {
console.log('Header was clicked');
};
const handleButtonClick = (event) => {
event.stopPropagation();
console.log('The logout button was clicked');
};
// The code down here remains the same
}
export default App;
We first passed the event
object as a parameter to the handleButtonClick
function and then used event.stopPropagation()
to stop the event from bubbling into the parent component. Now, if we click the button, no bubbling occurs, and we only get the log for the button component. It should look like this:
Event delegation in React v16 vs. React v17
In React v16 and earlier, event delegation was handled by attaching event handlers at the Document node per event. This meant React would figure out which component was called during an event and bubble that event to the Document node where React had attached event handlers.
There were issues with this method because of the nested React trees. If you have nested React trees in your application, the event.stopPropagation()
method will not work as intended. Let's look at an example below:
// Nested React trees in the index.html
<div id="parent">
<div id="root"></div>
</div>
Paste the following code into the App.js
file:
import "./styles.css";
import React from "react";
export default function App() {
const handleButtonClick = () => {
console.log("The button was clicked");
};
return (
<div className="App">
<button onClick={handleButtonClick}>Click me</button>
</div>
);
}
In the code block above, we have a button
with an onClick
handler that points to a function that logs some text to the console when clicked.
In the index.js
file, paste the code below into it:
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
const parent = document.getElementById("parent");
parent.addEventListener("click", (e) => {
console.log("The parent was clicked");
e.stopPropagation();
});
ReactDOM.render(<App />, document.getElementById("root"));
In the code block above, we got the parent
element from the DOM and attached an EventListener
to it. The EventListener
listened for a click
event and logged a different text to the console. Then, we called the event.stopPropagation()
method in the following line.
We expect to get two different text strings when we click the button. But, instead, we get one, and it's not even the one that we are expecting, as shown in the image below:
This happens because when you have nested React trees, React will still bubble events to the outer tree even after event.stopPropagation()
has been called.
Changes to the DOM tree
In React v17 and later, the developers of React changed how it attaches events to the DOM tree. The event handlers are now attached to the root DOM container that renders your React tree and is no longer attached to the Document node. Here’s a visualization of those changes:
If we change the index.js
file to reflect React v17 changes, we get the appropriate logs in the console, as shown below:
// index.js
import { createRoot } from "react-dom/client";
import App from "./App";
const parent = document.getElementById("parent");
parent.addEventListener("click", (e) => {
console.log("The parent was clicked");
e.stopPropagation();
});
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(<App />);
Now, when we click the button, we get the logs, as shown in the image below:
Conclusion
This article looked at how events are handled in React. We looked at event delegation, propagation, bubbling, capturing, and SyntheticEvents
in React. In addition, we looked at the event.stopPropagation()
method, and how we can use it to stop event propagation. We also looked at how events were handled in previous versions of React and how events are handled in the current versions of React.
LogRocket: Full visibility into your production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?
Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.
No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
Top comments (0)