DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 963,673 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Elad Tzemach
Elad Tzemach

Posted on

Event Capturing and Bubbling in React

Imagine you have the following code:

  const [counter, setCounter] = useState(0);
  return (
    <div onClick={() => setCounter((prevCounter) => prevCounter + 1)}>
      <button onClick={() => setCounter((prevCounter) => prevCounter + 1)}>
        Counter value is: {counter}
      </button>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

That renders this button:

Screen Shot 2020-12-10 at 10.27.49 AM

What would be displayed on that button if you click it?

If you guessed "Counter value is: 1", you were wrong!

What we get is this:

Screen Shot 2020-12-10 at 10.30.19 AM

But why?

Understanding Event Propagation

In our example, although we clicked on the button, the event handler of its div parent was triggered as well. That's because events don't just affect the target element that generated the eventβ€”they travel up and down through the DOM tree to reach their target.
This is known as event propagation: a mechanism that defines how events propagate or travel through the DOM tree to arrive at its target and what happens to it afterward.

The concept of event propagation was introduced to deal with the situations in which multiple elements in the DOM hierarchy with a parent-child relationship have event handlers for the same event, such as a mouse click. Now, the question is which element's click event will be handled first when the user clicks on the inner element: the click event of the outer element, or the inner element?

Event Propagation has three phases:

  1. Capturing Phase - the event starts from the window down until it reaches the event.target.
  2. Target Phase - the event has reached the event.target. The most deeply nested element that caused the event is called a target element, accessible as event.target.
  3. Bubbling Phase - the event bubbles up from the event.target element up until it reaches the window, meaning: when an event happens on an element, it first runs the handlers on it, then on its parent, then all the way up on other ancestors. That's the reverse of what is happening in the Capturing Phase.

hjayqa99iejfhbsujlqd

Event Bubbling and Capturing in React

Bubbling and capturing are both supported by React in the same way as described by the DOM spec, except for how you go about attaching handlers.

Bubbling is as straightforward as with the normal DOM API; simply attach a handler to an eventual parent of an element, and any events triggered on that element will bubble to the parent, just like in our example in the beginning.

Capturing is just as straightforward, but instead of the onClick prop, you have to use onClickCapture on your element.

How Do You Stop an Event from Bubbling/Capturing?

Going back to our original example, how can we make sure our counter is only incremented by 1 when we click the button?

The answer is using stopPropagation()
This method of the Event interface prevents further propagation of the current event in the capturing and bubbling phases.
It does not, however, prevent any default behaviors from occurring. (if you want to stop those behaviors, you would need to use the preventDefault() method)

If we change our code to:

  const [counter, setCounter] = useState(0);
  return (
    <div onClick={() => setCounter((prevCounter) => prevCounter + 1)}>
      <button
        onClick={(event) => {
          event.stopPropagation();
          setCounter((prevCounter) => {
            return prevCounter + 1;
          });
        }}
      >
        Counter value is: {counter}
      </button>
    </div>
Enter fullscreen mode Exit fullscreen mode

Our counter will be incremented by 1 each time we click the button, thanks to event.stopPropagation() which prevents the event from bubbling up to button's parent and triggering the parent's onClick as well.

However, be careful when stopping events from propagating, because sometimes you can’t really be sure you won’t be needing the event above in one of the element's parents, maybe for completely different things.

If this is the case, one alternative to stopping the propagation is writing your data into the event object in one handler and read it in another one, so you can pass to handlers on parents information about the processing below.

Happy coding!! πŸš€

Top comments (5)

Collapse
 
varuns924 profile image
Varun S

Very useful article! Can you explain further what you mean by "writing your data into the event object in one handler and read it in another one, so you can pass to handlers on parents [sic] information about the processing below"? Why wouldn't you just write the data and read it in a single handler?

Collapse
 
eladtzemach profile image
Elad Tzemach Author • Edited on

I'm happy to hear you have found this article to be useful!

In regards to your question - imagine you have the following:

<div onClick=...>
   <span id="1" onClick=...> ... </span>
   <span id="2" onClick=...> ... </span>
   <span id="3" onClick=...> ... </span>
</div>
Enter fullscreen mode Exit fullscreen mode

If you want the onClick of both the span and div to be triggered when you click on the span, except for when it's the span with id of 2, how can you achieve that?

My suggestion was that one option is to write information to the event object of that span, so then the div event handler can read it, see that it's the span with id of 2, and then not trigger its own event handler.

I hope it makes sense!

Collapse
 
aderchox profile image
aderchox

How do you write information to the event object? With event.someProp = value?

Collapse
 
varuns924 profile image
Varun S

Yes, I now see the application, thank you!

Collapse
 
alexmiguel95 profile image
Alex Miguel

Thanks!! This post saved me!

Need a better mental model for async/await?

Check out this classic DEV post on the subject.

β­οΈπŸŽ€ JavaScript Visualized: Promises & Async/Await

async await