Skip to main content Accessibility Feedback

How to more performantly listen for the same event across different scripts with JavaScript

Today, we’re going to talk about how to manage event listeners when you have multiple scripts that listen to the same event.

Let’s imagine you have a few different scripts in your app that all need to detect click events. One script opens modals. Another toggles some disclosure/expand-and-collapse elements. A third saves form data.

When you’re listening for the same event on multiple elements, event delegation is an excellent strategy!

document.addEventListener('click', function (event) {

	// If the clicked thing isn't an element we care about, bail
	if (!event.target.matches('[data-modal="#terms"]')) return;
	
	// Otherwise, run your code...
	
});

But how do you implement event delegation when you have multiple scripts or separate tasks that all rely on the same event?

Let’s dig in!

Approach 1: multiple event listeners

One approach is to use multiple event listeners, one for each selector.

document.addEventListener('click', function (event) {
	if (!event.target.matches('[data-modal]')) return;
	// Toggle the modal...
});

document.addEventListener('click', function (event) {
	if (!event.target.matches('[data-disclosure]')) return;
	// Show the disclosure content...
});

document.addEventListener('click', function (event) {
	if (!event.target.matches('[data-save-form]')) return;
	// Save the form content...
});

This is the approach that I typically use when I have separate, discrete scripts bundled as libraries or standalone functions.

While you’re using multiple event listeners instead of one, this is still far better for performance than attaching a listener to every single matching element.

Approach 2: conditionals

If both pieces of code are in the same file and part of the same broader script, you can combine them into a single event listener and use a conditional check, like an if...else statement or break.

document.addEventListener('click', function (event) {
	if (event.target.matches('[data-modal]')) {
		// Toggle the modal...
	} else if (event.target.matches('[data-disclosure]')) {
		// Show the disclosure content...
	} else if (event.target.matches('[data-save-form]')) {
		// Save the form content...
	}
});

To keep things more readable, you can move the code you want to run for each condition to its own separate function, and pass the event object or event.target in as an argument.

document.addEventListener('click', function (event) {
	if (event.target.matches('[data-modal]')) {
		toggleModal(event);
	} else if (event.target.matches('[data-disclosure]')) {
		showDisclosure(event);
	} else if (event.target.matches('[data-save-form]')) {
		saveForm(event);
	}
});

Approach 3: the early return pattern

A lot of people find if...else or break statements unwieldy, especially if you have more than just a couple of checks. They can quickly grow out of hand.

For that reason, I prefer to use the early return pattern.

document.addEventListener('click', function (event) {

	if (event.target.matches('[data-modal]')) {
		toggleModal(event);
		return;
	} 

	if (event.target.matches('[data-disclosure]')) {
		showDisclosure(event);
		return;
	}

	if (event.target.matches('[data-save-form]')) {
		saveForm(event);
		return;
	}

});

This approach is, to me, easier to read. It also means I can move my checks around without worrying about breaking anything.

Approach 4: early return in handler functions

Instead of using a bunch of if statements with early returns, you can instead run each of your handler functions every time, and move the early return inside the handler.

function toggleModal (event) {
	if (!event.target.matches('[data-modal]')) return;
	// Toggle the modal...
}

function showDisclosure (event) {
	if (!event.target.matches('[data-disclosure]')) return;
	// Show the disclosure content...
}

function saveForm (event) {
	if (!event.target.matches('[data-save-form]')) return;
	// Save the form content...
}

document.addEventListener('click', function (event) {
	toggleModal(event);
	showDisclosure(event);
	saveForm(event);
});

This approach provides a nice balance of performance and maintainability.

It’s slightly less performant, in that showDisclosure() and saveForm() would still run even if the clicked item was a modal toggle. But I wouldn’t expect any real world performance implications.

And I think this provides a more clean and readable structure than some of the other options.