Event bubbling and capturing in React
Event propagation is a method of managing events that involves attaching a single event listener to a parent element, rather than assigning event listeners to each child element. When an event occurs on a child element, the parent element's event listener is notified, and it can handle the event. This approach is helpful when there are numerous child elements, and you want to avoid assigning event listeners to all of them.
Let's suppose you have the following code as an example:
function App() {
const parentFunction = () => {
console.log('Parent');
};
const firstChildFunction = () => {
console.log('First child');
};
const secondChildFunction = () => {
console.log('Second child');
};
const thirdChildFunction = () => {
console.log('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;
Upon clicking any of the buttons and reviewing the console, we can observe that the log for the second button is displayed. However, we also notice an extra log indicating that the parent div was clicked.
This suggests that event propagation occurred, where the parent element handles the event of the child element.
Now, let's explore the order in which React manages the event.
Event propagation
Event propagation, which refers to the order in which event handlers are called when an event occurs on a webpage. There are three phases of event flow in the DOM: the capturing phase, the target phase, and the bubbling phase. The capturing phase involves the event propagating from the top of the document down to the element on which the event occurred. The target phase executes the event handler on the element where the event occurred. Finally, in the bubbling phase, the event moves back up the DOM tree from the element where the event occurred to its parent element until it reaches the top of the document. The default behavior for most events in the DOM is the bubbling phase. The text includes an image that illustrates the three phases of event propagation:
Synthetic Events
React handles DOM operations by relying on a virtual DOM. It uses a SyntheticEvent wrapper to abstract differences between browser events, allowing for consistent event handling across all browsers. This feature ensures that event handlers written in React work seamlessly in all supported browsers. See below an example code snippet of a click event listener attached to a button.
document.getElementById('button').addEventListener('click', () => {
console.log('I was clicked!');
});
In the above code, the click event is a native DOM event. In React, you would write the event as follows:
<button onClick={() => console.log('I was clicked!')}>Click me</button>
React's onClick event is a synthetic event that calls the provided event handler function when the button is clicked. React converts the synthetic event into a native DOM event and passes it to the event listener. SyntheticEvents help write event handlers in a familiar way, without worrying about differences between various browser events. Additionally, SyntheticEvents include methods like stopPropagation and preventDefault, which are critical to React's Event System.
How to use event.stopPropagation()
It prevents an event from being handled by the parent component if a child component handles it. The method is called on the event object to stop the event from propagating up the tree. This feature is useful when a parent component has an event handler that conflicts with the child component's event handler.
See the following example of a header component with a button for logging out and how the event.stopPropagation() method can be used to avoid conflicts between the parent and child components:
function App() {
const handleHeaderClick = () => {
console.log('Header clicked');
};
const handleButtonClick = () => {
console.log('The login button was clicked');
};
return (
<>
<div onClick={handleHeaderClick}>
<div>Header</div>
<button type="button" onClick={handleButtonClick}>
Login
</button>
</div>
</>
);
}
export default App;
There are cases where we don't want the parent component to handle an event, such as when a child component has its own event handler. If the parent component still handles the event in such cases, we can use the event.stopPropagation() method to stop it. See the example below:
function App() {
const handleHeaderClick = () => {
console.log('Header clicked');
};
const handleButtonClick = (event) => {
event.stopPropagation();
console.log('The login button was clicked');
};
// ...
}
export default App;
Use the event object parameter and the event.stopPropagation() method to stop event bubbling in a React component. The example code shows that the event object is passed as a parameter to the handleButtonClick function, and the event.stopPropagation() method is used to stop the event from bubbling into the parent component. As a result, when the button is clicked, no bubbling occurs, and only the log for the button component is displayed.
Event propagation in React v16 vs. React v17
In React v16 and earlier, event propagation was managed by attaching event handlers at the Document node per event. This meant that when an event occurred, React would determine which component was called during the event and bubble that event to the Document node where React had attached the event handlers.
The event.stopPropagation() method with nested React trees can cause issues in event propagation. See the following code:
// Nested trees
<div id="parent">
<div id="root"></div>
</div>
import React from "react";
export default function App() {
const handleButtonClick = () => {
console.log("Button clicked!");
};
return (
<div className="App">
<button onClick={handleButtonClick}>Click me</button>
</div>
);
}
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"));
The event.stopPropagation() method does not work as intended in nested React trees. An EventListener is attached to a parent element, and event.stopPropagation() is called, but the expected behavior does not occur. Instead of two different text strings being logged when a button is clicked, only one is logged, and it is not the expected one.
In React v16 and earlier, event propagation was handled by attaching event handlers at the Document node. However, this method had issues when it came to nested React trees. The event.stopPropagation() method did not work as expected. In React v17 and later, changes were made to how events are attached to the DOM tree. Event handlers are now attached to the root DOM container that renders the React tree and no longer to the Document node.
If we change the index.js file to reflect React v17 changes, we get the appropriate logs in the console, as shown below:
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.
Conclusion
This article provides an overview of how events are handled in React. It covers topics such as event propagation, propagation, bubbling, capturing, and SyntheticEvents. It also explains how to use the event.stopPropagation() method to stop event propagation and discusses the changes made to event handling in React v17 and later versions.