Bubble Events in React

This article explains how to bubble events up the DOM to a single event listener in React.

Event Bubbling

In DOM, an event is sourced on a particular element. That is the event target. If you click a button, that is the source. If you submit a form, that is the source.

An event that originates on a child node in the DOM tree will automatically "bubble up" (aka propagate), meaning, visit every parent node, all the way up to the root document node.

If you wanted to capture all click events for the whole document, you could register a click handler on the document, and run a callback for any click on anything at any level of the tree. For example:

document.addEventListener('click', (evt) => console.log(evt))

The Bubbly is Nice

There are a couple reasons to like event bubbling.

In React, it is common to set event handlers directly onto the originating element (eg, a button gets a click handler). But this means that the handler reference is probably sent as props to the component. If you didn't have to send the handler as props, that's one less prop to send. Fewer props, cleaner component.

Let's say that we have a list of things with buttons that need handlers. In React, because it's common to set the handler on the button, we now have n handlers, n being the length of the list. And these is newly-defined functions for each button. Agh! For example:

return rows.map(row => 
  <button onClick={() => myHandler(row.id)}>Click me</button>)

If the handler is outside of the loop, fewer handlers are defined. More performant.

Different prop for Bubbling

When you set a click handler, you usually use the React prop of onClick.

When you capture bubbled events, you use onClickCapture.

Where do you put that prop? Well, you aren't going to have access to document.

Put it as high up the DOM/component tree as makes sense for the handler you're attaching. Use a prop like this:

<div onClickCapture={myHandler}>

Listener for All Clicks

Usually you're putting a click handler directly on a button or a submit handler directly on a form. But if you let your event bubble, you're not specific now.

A pitfall: Often, you run evt.preventDefault() in your click handler right away. But in the bubbling case, you'll want to avoid that. If you don't, you'll end up doing things like breaking all links in your DOM subtree, preventing them from acting like links.

Filter Clicks

Instead, you'll want to look for the events that your handler really cares about. How will you identify them? I like using data- attributes/props on the button (or whatever) elements.

Back to our list of buttons example:

return rows.map(row => 
  <button data-myclick-id={row.id}>Click me</button>)

Now you want to see if the thing that was clicked is a "myclick" button. You could look for the data attribute:

function myHandler(evt) {
  if ('myclickId' in evt.target.dataSet) {
    evt.preventDefault()
    // handle myclicks
  }
}

But, if there's an icon or sub-node inside of the button that you click, it won't be truthy. So it's better to check to see if the click comes from within the button, using the .closest() function:

function myHandler(evt) {
  const button = evt.target.closest("[data-myclick-id]");
  if ('myclickId' in button.dataSet) {
    evt.preventDefault()
    // handle myclicks
  }
}

Pass Data to Handler

Once we know that we want to handle an event, how do we pass data related to that event to the handler? One great way would be via data- attributes, the same way that we identified the event in the first place.

For example, if the button has a data-myclick-id, pull that id and use it in your event handler, like this:

const myClickId = evt.target.dataSet.myClickId

From this point, handling an event should feel familiar, much like you would with a normal click handler. But now, bubbled!