Introduction
Event delegation is a powerful JavaScript technique that allows you to use a single event listener on a parent element to manage events for multiple child elements. This approach improves performance, simplifies your code, and makes it easier to maintain. Instead of attaching separate event listeners to each individual child element, event delegation leverages the event bubbling mechanism in JavaScript, where events "bubble up" from the target element to its ancestors in the DOM.
By using this technique, you can achieve similar functionality for many child elements with just one event listener. This not only reduces memory usage and improves app performance, but it also makes your code cleaner and more scalable.
For example, imagine you have a list of categories on your website. Instead of assigning an event listener to each category element, you can assign one event listener to the parent container of all categories. That listener will handle click events for any of the child elements, no matter how many categories you have. As your application grows and you add more categories dynamically, you don’t need to add new event listeners for each one — it just works.
Look at the example below, and you'll see how this works in practice:
Did you notice that as you click on any category, it redirects you to the specific page for that category? There are about five categories right now, but regardless of which category you click, you’re redirected to the corresponding category page. For example, clicking on laptop takes you to the /laptops page. All of this functionality is achieved using a single event listener on the parent element.
This is the power of event delegation. It’s not only more efficient but also reduces the risk of code duplication and makes the code easier to maintain and debug. Let’s take a look at the JavaScript code below:
// Select the parent element (category list container)
const categoryList = document.querySelector('.category-list');
// Attach a single event listener to the parent (event delegation)
categoryList.addEventListener('click', (event) => {
// Check if a list item was clicked
if (event.target && event.target.nodeName === 'LI') {
const category = event.target.getAttribute('data-category');
// Redirect to the category page
window.location.href = `/${category}`; //
}
});Have you seen the code? You can clearly see that there is only one event listener attached to the parent element, but it handles the click events for all child elements — in this case, the categories.
This technique is especially useful in situations where elements are dynamically added or removed, as you don’t need to keep track of each new child element separately.
Today, in this article, we will thoroughly explore this technique with examples, so you can understand its true value. We will cover both why and how in detail.
How does event delegation work?
As we already have a good understanding of event delegation, it's time to explore how it actually works — this is something that might come up in an interview. Besides that, understanding it is crucial for handling events effectively.
Event delegation works because of event bubbling, where events bubble up from the target element to its parent elements in the DOM tree. By leveraging this bubbling behavior, you can handle events on child elements — even if they are dynamically added or removed — by attaching the event listener to a common ancestor.
There are mainly three steps involved in event delegation, as outlined below:
Event Bubbling
When an event is triggered on a DOM element (e.g., a click event on a button), the event "bubbles"" up the DOM tree. It first triggers on the target element and then propagates (or bubbles) upwards to its parent elements, eventually reaching the root document.
Delegate the Event to a Parent
Instead of adding event listeners directly to each individual child element, you add a single event listener to a parent element.
Check the Target
In the event handler, use the event.target property to check which child element the event was originally triggered on. This allows you to handle events on dynamically added elements as well.
Now let's understand event delegation practically using code.
Take a look at the code below: I’ve used HTML to create a <ul> element with three <li> items. The <ul> is the parent element, and the three <li> elements are its children. Each <li> acts as a "card." The goal is to implement functionality where, when any card is clicked, its number increases by 1.
<ul class="parent-element">
<li class="child-element">0</li>
<li class="child-element">0</li>
<li class="child-element">0</li>
</ul>So far, we’ve only created the cards but haven’t added any functionality. Now we need to decide: should we add three separate event listeners (one for each card), or just one event listener on the parent element to handle them all?
To keep our code clean and efficient, we’ll use the event delegation approach. That means we’ll attach a single event listener to the parent (<ul>) and handle clicks on its children (<li>) through that. Check out the example below — when you click a card, its number increases by 1.
Did you notice how it works? Each card's number increases every time it is clicked. If you look in the main.js file, you’ll see that there's just one event listener.
Let’s break down how this event listener works:
First, the event listener is added to a variable called parentContainer, which holds the reference to the <ul> element. It takes a parameter e (short for event), which helps us determine which child element was clicked.
This e object is the key to event delegation — it gives us access to e.target, which tells us exactly which element triggered the event. That’s what makes event delegation powerful: instead of adding individual listeners, we handle everything through the parent.
Here’s what happens behind the scenes (As shown in the interactive diagram below): When you click any card, the event bubbles up from the clicked child element to the parent. The parent detects which child was clicked using e.target, and applies the logic (like incrementing the number) only to that specific child — not the others.
e.target = index
- Card 0
- Card 1
- Card 2
- Card 0
- Card 0
- Card 0
Have you noticed how the given card updates when it’s clicked? When you click on this card, the diagram shows the entire process happening behind the scenes. First, the click event bubbles up to the parent element. The parent element then identifies the source of the event using e.target, and after that, it triggers the appropriate functionality for the specific card that was clicked, updating that particular card.
I hope you understand the process and the philosophy behind this. Now, I want to clarify a few questions that may be on your mind.
If you're a somewhat experienced developer and have worked on a few JavaScript projects, you might be wondering:
Can’t we achieve the same functionality by attaching individual event listeners to each child element using querySelectorAll, instead of attaching a single listener to the parent?
The answer is yes — we can achieve similar functionality — but it's less efficient compared to using event delegation. Let’s explore why.
When we use querySelectorAll to select elements with a certain class (for example, .child-element), it returns a NodeList containing all the matching elements.
const children = document.querySelectorAll('.child-element');
console.log(children);
// Output: NodeList(3) [li.child-element, li.child-element, li.child-element]
This means the children variable holds a list of all <li> elements with the class child-element. You can then loop through this NodeList using forEach to attach event listeners or perform other operations.
To attach event listeners to each of these, we typically use a forEach loop. Inside that loop, we attach an event listener to each child. So, if there are three child elements, three separate event listeners are created in memory as soon as the page loads.
Although the functionality looks the same — clicking on a card still increases the count — this approach consumes more memory. That’s because each child element holds its own event listener. While that may not be a big deal with a small number of elements, it becomes inefficient and can lead to performance issues when dealing with a large number of elements.
On the other hand, with event delegation, we attach a single event listener to the parent element (such as a <ul>), and handle the child interactions using event bubbling. This is a much more memory-efficient and scalable approach, especially for dynamic or content-rich web applications.
That being said, using querySelectorAll and attaching event listeners to each child element means we are writing less code to achieve big or dynamic functionality. However, this approach is not as effective because, behind the scenes, as many event listeners are created as there are child elements after the page loads. While it may seem effective in terms of writing less code, it is not ideal for app performance and memory usage.
Now, you may have this question in your mind: When should we use the event delegation approach vs. querySelectorAll, where we attach event listeners to each child by writing inside a loop?
Even though event delegation is the top choice, this doesn't mean the second approach is completely useless. If there are a limited number of child elements (e.g., 10 to 15), we should use querySelectorAll because it is faster and more efficient in this case. With event delegation, the script has to check event.target and then proceed with the function, which introduces a small delay compared to directly attaching event listeners to each child.
However, if there are many child elements, event delegation is obviously the better approach. It doesn't load the memory with individual listeners, which helps prevent performance issues. Event delegation is a constant time solution for handling large numbers of child elements because it takes the same amount of time, regardless of how many child elements there are. In contrast, querySelectorAll has a linear time complexity, meaning it will consume more memory and resources as the number of child elements increases.