Noob Tidbit #5: Event Delegation

JavaScript Event Delegation and How it is Used in React

September 13, 2020

I first learned about event delegation when I went to Galvanize back in 2016. Unfortunately, it was taught when I was feeling rather overloaded by all the information being hurled at me during this intensive six month program. I kind of got it, but not completely. It wasn't until a few months into my first job when I had the chance to work with React in the real world that I found a need to revisit the concept. I had multiple click events attached to several children and was noticing some weird things happening when trying to handle the event. Why?

What is Event Delegation?

Event delegation is a process used to handle events in a level higher up in the DOM than where the event actually occurred. This is very powerful as it means you can create a single event listener on a parent node that will handle events fired on any of its children. Without event delegation, each event would need to have its own event listener which would make monitoring events across an application far less streamlined and performant.

Imagine if you wanted to add a table to a page and needed to track a click event on all the individual cells.

A table with multiple cells.

You could add event handlers to every single cell.

Event handling without delegation.

But that's definitely not optimal and will only get worse with every cell you need to add. Let's look at an updated table where the event delegation pattern has been applied.

Single event handler on a table.

Now, from within the handler, you can access the event and whatever information is attached to it. For instance, we can now access the text of every cell.

Catch all event handler.

As you can see, event delegation is incredibly powerful and makes it easy for us to write performant, DRY code.

How Event Delegation Works

The event listener on the parent is always lying in wait, watching over all of its children and their events, waiting to respond when an event is fired somewhere in the DOM.

Table element watching over children.

When an event is fired on an element, now known as the target element, the process of event delegation begins. This event handling pattern is made up of two processes: event bubbling and event capturing.

Event Bubbling

This is the most commonly used of the two processes. It's called bubbling because if you visualize a bubble in the water as it makes its way to the surface, it becomes really easy for you to then visualize an event being fired on an element, bubbling up to its parent, then up to its ancestors, and then continuing its journey all the way up to the document object. Apart from the focus and blur events, which trigger on an element focusing/losing focus, all events that fire will bubble in this fashion.

bubbles traveling to surface

In the following screenshot we can imagine a user clicking on one of the cells, the event bubbling up to its parent row (tr), and then continuing to bubble up to the table element where we have placed the handleCellsClick event handler. The table element is the common ancestor for all the table cells.

Event fired on target cell.

When an event is fired on an element, the parent's handler is able to get more details about where the event occurred by utilizing event.target.

Accessing the event via the target.

Now let's say we wanted to get specific event information from the first row but then different event data for all remaining rows. For the second row onwards, we could still utilize our handleCellsClick event handler that we attached to the table element. For the first row, we'd want to add a separate handler.

Event handler on first row.
handleRowClick event handler

The issue with this right now is that anytime someone clicks on a cell in the first row both the handleRowClick and handleCellsClick events would fire.

2 event handlers firing.

This happens because when the target element is clicked, in this case cell 3, the event bubbles up to the handleRowClick event handler on the tr element, and then continues on to the handleCellsClick event handler on the table element.

In order to prevent the event from bubbling to handleCellsClick, which is an event handler higher up in the DOM tree, we can simply add event.stopPropagation.

Using stopPropagation

We can see our log coming through from handleRowClick but we no longer see the log coming from handleCellsClick as the bubbling is now being stopped at the row element thanks to event.stopPropagation.

An important thing to keep in mind is that unnecessary use of event.stopPropagation can cause issues for analytics. If you're effectively blocking the bubbling from reaching the surface at certain points in your application, certain analytics, such as user click count, won't be able to report the complete picture. Apply you propagation stopping wisely :)

What is Event Capturing?

In the simplest sense, capturing is the opposite of bubbling where the event trickles down to the element to 'capture' the event on the targeted element. From here, the bubbling begins.

While as straightforward as bubbling, capturing is normally invisible to us and is rarely used the way we use bubbling to handle events. When someone clicks on a target element, for instance, the event will go down the ancestor chain to where the event was fired. We don't see this happen so, for those who are unfamiliar with event delegation and capturing, it may appear that the event actually starts on the element and simply bubbles up.

Capturing phase.

There aren't a whole lot of use cases as to why you would want to utilize the capturing case explicitly in your code. However, if you're hoping to listen to events that don't bubble, such as focus and blur, then you could do this during the capturing phase.

To register an event on the capture phase instead of the bubbling stage, you need to you need to append 'Capture' to the end of the event name.

onClickCapture
You can see in this example, we've added an onClickCapture event handler to the parent element. When the button is clicked, we only see 'Capturing the className of the target element: target-element' as the process stops inside our handleCapture function. We are getting data for the target element from the capture phase and then preventing the bubbling process from occurring.

Not capturing event.
For comparison, if we change onClickCapture to onClick and then click the button, we see logs from both events in the console.

Custom Event Delegation

There's one other thing I want to touch on before wrapping up and that is custom event delegation. This is where you add custom data to the event object.

Adding custom data to the event object.

While this may seem like a useful way to pass data around, it is discouraged as it goes against React's philosophy. React has a one-way data flow and adding custom data to the event object introduces a secondary data flow. Having multiple data flows becomes problematic to manage as an application grows, and debugging errors that take place on the event is complicated. Additionally, instead of explicitly passing data by the way of props, you introduce an implicit dependency whereby data is also be passed on the event. Avoid doing this and stick to passing data the way it is designed to be passed.

Share this post: