Kyle Shevlin

Software Engineer
June 18, 2022
0 strokes bestowed

Capture Phase Event Handling in React

edit

So... as of the time I'm writing this, I've used React for 7 years and today (February 22, 2022) I learned something completely unbeknownst to me. You can append Capture to an event name to have it handled in the "capture" phase of event delegation, instead of the "bubbling" phase. Who knew?! It's right here in the docs.

So what is this and why is it useful? Let's explore.

What is event capturing and bubbling?

In short, because there are a million other articles on the topic, event capturing and bubbling describe how events traverse the DOM. Look at the following markup:

<html>
  <head>
    Event capturing and bubbling
  </head>
  <body>
    <div>
      <button>Click me</button>
    </div>
  </body>
</html>

When you click the button, the "capture" phase begins. It starts at the top of the DOM, and traverses down the tree through each element. When it reaches the target, the button, it begins the "bubbling" phase and traverses back up to the top of the DOM tree. We can imagine that loop like this:

ElementEventPhase
htmlclickcapture
bodyclickcapture
divclickcapture
buttonclickcapture
buttonclickbubble
divclickbubble
bodyclickbubble
htmlclickbubble

We can see this by writing just a few lines of code:

// Wildcard selector
const allElements = document.querySelectorAll('*')

function logNodeName() {
  // I know, I hate using `this` too
  console.log(this.nodeName)
}

for (const el of allElements) {
  // This one will be used in the capture phase
  el.addEventListener('click', logNodeName, true)
  // This one will be used in the bubbling phase
  el.addEventListener('click', logNodeName)
}

Now I've attached the logNodeName function as a click event handler to every element on the document. That means, clicking anywhere, should log out all the elements touched in the "bubbling" phase. Note: this does not remove any listeners, so it's possible to have memory leaks if you add or remove elements from the page between clicks. Try it out here:

Check out the console to see the logs after clicking.

*Disabling will prevent "noise" in the console which might be useful in the rest of the post.

You can see that it starts at the html element and works its way down to the button and then works its way back up. Honestly, because I used the wildcard selector, *, you can click anywhere and see the output. Have fun.

Using the capture phase with React events

In most cases, React abstracts the process of attaching event handlers to elements. Rather than getting the element and writing addEventListener and removeEventListener, we use the associated element attribute for the given event we want to respond to, such as onClick.

const button = document.getElementById('#btn')
button.addEventListener('click', () => { console.log('clicked') })

// Vs.
<button onClick={() => { console.log('clicked') }}>Click me</button>

addEventListener and removeEventListener receive an optional third argument of the type boolean | Options. I will ignore the Options object as you can look that up here and instead focus on the boolean that represents the useCapture value.

By passing true into this third argument, we indicate that this event handler should be used in the capture phase. But, if we're using React's abstractions, we have no place to pass in a useCapture argument. Do we?

We do.

This is where appending Capture to the event handler name comes in. If we want a click handler to be used in the capture phase, rather than the bubbling phase, we use onClickCapture instead. Let's make an example.

Let's say we have a group of buttons and every button should use the exact same event handler. Rather than attach the same handler to each button, we can attach it to the parent element in the capture phase. I'm eating breakfast as I write this, so let's imagine a rudimentary menu for selecting a breakfast item.

const ITEMS = ['Eggs', 'Bacon', 'Pancakes', 'Toast']

function Breakfast() {
  const [selected, setSelected] = React.useState(null)

  const selectItem = e => {
    setSelected(e.target.value)
  }

  return (
    <div>
      <div>Selected: {String(selected)}</div>
      <div onClickCapture={selectItem}>
        {ITEMS.map(item => (
          <button key={item} value={item}>
            {item}
          </button>
        ))}
      </div>
    </div>
  )
}

See it in action here:

Selected breakfast food: null

Now, before you judge the example too harshly for being rudimentary, let me be abundantly clear: This isn't the only way, or even the recommended way, to accomplish this. It's just to teach the concept. That said, I think it's kind of neat that we can do this without onClick handlers on each button.

Additional thoughts

This technique is a bit of a throwback. Back to a time where maybe you were using <input type="button"> or inlining an onclick attribute on a parent. It's unlikely in React and other modern frontend frameworks that you'll ever have to use *Capture events.

However, that doesn't mean it's not worth throwing this into your repertoire! You never know when you might run into a situation where this little tid bit is the simplest and most elegant way to solve the problem.


Finished reading?

Here are a few options for what to do next.

Like
Liked the post? Click the beard up to 50 times to show it
Share
Sharing this post on Twitter & elsewhere is a great way to help me out

Older Post: Why Use useReducer?
Tags
React

Kyle Shevlin's face, which is mostly a beard with eyes
Kyle Shevlin is a software engineer who specializes in JavaScript, React and front end web development.

Let's talk some more about JavaScript, React, and software engineering.

I write a newsletter to share my thoughts and the projects I'm working on. I would love for you to join the conversation. You can unsubscribe at any time.

Data Structures and Algorithms Logo
Data Structures and Algorithms

Check out my courses!

Liked the post? You might like my courses, too. Click the button to view this course or go to Courses for more information.
I would like give thanks to those who have contributed fixes and updates to this blog. If you see something that needs some love, you can join them. This blog is open sourced at https://github.com/kyleshevlin/blog
alexloudenjacobwsmithbryndymentJacobMGEvanseclectic-codingjhsukgcreativeerikvorhesHaroenvmarktnoonandependabotmarcuslyonsbrentmclarkfederico-fiorinimedayzDoNormalFanchGadjonoahmateenbrandonpittman
©2022 Kyle Shevlin. All Rights Reserved.