A Beginner’s Guide to JavaScript Event Delegation

Event delegation in JavaScript.

Don’t Repeat Yourself.

DRY.

It’s one of the most common mantras repeated to, and by, newbie programmers. Refactoring code to minimize repetition has many tangible benefits. It makes code easier to read, understand and revise. It reduces load times and creates faster apps. Most importantly for students, it solidifies conceptual understanding and helps a portfolio piece stand out to potential employers.

Event delegation is often a great way to start refactoring novice JavaScript code. This allows you to place a single EventListener on a parent element that does the work of a few, or dozens, or even hundreds of EventListeners. It makes code so DRY you might as well move to Moab and start a sandpaper business.

I created the world’s simplest social media website to provide an example. Below you’ll find the HTML for a single post from an anonymous user who ate some delicious tacos for lunch. Readers can react with a Like, a Dislike or a Laugh by clicking on one of three emoji buttons, each with its own ID. All three buttons are held within a div container.

HTML

<!DOCTYPE html> 
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event Delegation</title>
<script src="index.js" defer></script>
</head>
<body>
<p class="post">I ate tacos for lunch today.</p>
<div id="button-container">
<button id="like-button">👍</button>
<button id="dislike-button">👎</button>
<button id="laugh-button">🤣</button>
</div>
</body>
</html>

This renders on the DOM like so:

We want a counter to add ‘+1’ to each button every time it’s clicked. To start, the most logical solution might be adding an EventListener on each button element:

JavaScript (pre-delegation)

const $likeButton = document.querySelector('#like-button'); 
const $dislikeButton = document.querySelector('#dislike-button'); const $laughButton = document.querySelector('#laugh-button');
let dislikesCounter = 0;
let likesCounter = 0;
let laughsCounter = 0;
$likeButton.addEventListener('click', () => {
$likeButton.innerText = `👍${likesCounter += 1}`;
})
$dislikeButton.addEventListener('click', () => {
$dislikeButton.innerText = `👎${dislikesCounter += 1}`;
})
$laughButton.addEventListener('click', () => {
$laughButton.innerText = `🤣${laughsCounter += 1}`;
})

Now, when we click one of the buttons, it’ll increase that button’s counter.

Hey, it works! But what if we can do the same job with only a single EventListener? That’s the power and beauty of event delegation.

(Note: A basic understanding of event propagation — the capture phase, the target phase and the bubble phase — is beneficial, though not strictly necessary to follow along. I recommend this blog by Dmitri Pavlutin for a more in-depth explanation of what’s happening under the hood.)

Instead of each individual button, let’s select the button-container div and add an EventListener higher up on the DOM tree. Passing in event as an argument allows you to access all sorts of attributes through the event.target property. We can select each button based on its innerText, textContent, className, attribute type, and so on, by chaining it together with syntax such as event.target.className or event.target.textContent. Since each button in our example is already assigned an ID, let’s use that to select them. Console.log(event.target.id) shows what’s being targeted and why.

document.querySelector('#button-container').addEventListener('click', (event) => {
console.log(event.target.id);
})

Clicking each of the buttons will return the assigned button ID from the HTML document in the console. Since we can grab the button IDs through this higher-level EventListener, all that’s left to do is iterate through them and add actions based on the return values of event.target. It’s the same logic as before, just using a loop, if/else statements or a switch statement.

JavaScript (with delegation)

let likesCounter = 0; 
let dislikesCounter = 0;
let laughsCounter = 0;
document.querySelector('#button-container').addEventListener('click', (event) => {
switch (event.target.id) {
case 'like-button':
event.target.innerText = `👍${likesCounter += 1}`;
break;
case 'dislike-button':
event.target.innerText = `👎${dislikesCounter += 1}`;
break;
case 'laugh-button':
event.target.innerText = `🤣${laughsCounter += 1}`;
}
})

This works the same as the pre-delegation JavaScript, but with three fewer lines of code and a single EventListener instead of three. That might not seem like a huge deal with such a simple example, but imagine using this concept to replace 100 EventListeners instead of a handful.

To conclude with a real-world case study, for my Flatiron School Mod 3 project I built a website that allows users to log their ascents of Colorado’s 14,000-foot mountains. From an index with 58 mountain cards (one for each peak), a user can click on a card to visit a show page for each summit and leave a comment about recent hiking conditions. I initially accomplished this redirect by using a forEach loop on the array of mountain objects to add an EventListener to every individual card.

By using event delegation, however, I was able to replace those 58 EventListeners with the below five lines of code. Clicking each card — either on the surrounding span or on the actual H3 text — takes a logged-in user to the selected mountain show page via query params and allows them to leave a comment.

document.querySelector('#summits-container').addEventListener('click', (event) => {
if (event.target.tagName == "SPAN") {
window.location.href = `/mountain.html?
mountain=${event.target.id}&user_id=${id}`;
}
})

That’s event delegation! It’s one of the cooler and most helpful concepts I’ve learned so far in 10 weeks of coding school. Happy refactoring.

Full-stack developer. Full-stack adventurer.