DEV Community

Cover image for Event bubbling and capturing in React
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Event bubbling and capturing in React

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

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;
Enter fullscreen mode Exit fullscreen mode

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: React Child Component

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:

React Event Propagation Diagram

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');
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

The code block above will display the header component as shown in the image below: Example of React Header Logged

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: React Event Clicked

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;
Enter fullscreen mode Exit fullscreen mode

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:

React Event Capture Example

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>
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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: Example of React Bubbling Tree

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: A Diagram Showing How React v17 Attaches Events to the Roots Rather Than to the Document

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 />);
Enter fullscreen mode Exit fullscreen mode

Now, when we click the button, we get the logs, as shown in the image below: React Event Capturing and Bubbling

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 signup

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)