Custom Event Listeners in JavaScript

Custom Event Listeners in JavaScript

Dispatching custom events natively
Ferenc Almasi β€’ Last updated 2021 September 18 β€’ Read time 6 min read
Custom event listeners in JavaScript can help you achieve communication between two separate components. Learn how you can implement them.
  • twitter
  • facebook
JavaScript

Most of the time (apart from simple todo applications), when you are writing a modern web application, you have to separate code into different modules to keep things manageable. This makes it easy to separate the different behaviors of your application, as well as make it more easily scalable not to mention readability.

But what happens when you have to achieve communication between two separate components? That’s where custom event publishing and custom event listeners come into place. It provides a similar behavior to the PubSub design pattern.

PubSub in JavaScript: Intro to Design Patterns

What Are Events?

So what exactly are events? As you may already know, JavaScript is event-driven which means that the flow of the application is usually determined by events, such as various user interactions (clicks, keyboard input, scrolling, etc.).

There are tons of events you can attach to. Mouse alone has 15 different events you can listen to.

How do they work?

Events can be handled in two different ways. They are called event bubbling and capturing.

By default, events are using bubbling. When you attach an event to an element using addEventListener, the event is first captured in the innermost element and then propagates up to the root of the document.

Copied to clipboard!
const ul = document.getElementById('ul');

// By default, addEventListener uses event bubbling
ul.addEventListener('click', () => console.log('πŸ‘¨'));
event-propagation-model.js

The exact opposite happens during the capturing phase. The event is first captured in the outermost element and propagates to inner elements.

These two different propagation models can help us decide which element should receive the event first, given that the same event is registered for both a child and parent element.

Copied to clipboard! Playground
// To use event capturing for capturing events
// on parents first, pass true as the 3rd parameter 
// to addEventListener

const ul = document.getElementById('parent');
const li = document.getElementById('child'); 

ul.addEventListener('click', () => console.log('πŸ‘¨'), true);
li.addEventListener('click', () => console.log('πŸ‘Ά'), true);

// Instead of seeing πŸ‘Ά, πŸ‘¨ in the console
// You'll see πŸ‘¨, πŸ‘Ά as the direction is changed with `true`
event-propagation-model.js

To experiment with the two propagation models, take a look at this Codepen example:

codepen.io

Overall, there are almost 200 different standard events you can listen to. Yet, there could be times when you need a custom event thrown by your application. This is where custom events can be super useful.


Dispatching Custom Events

To start, let’s see how you can create custom events. All you have to do is call the Event constructor with the name of the event you want to dispatch:

Copied to clipboard!
// Pass the name of the event you want to dispatch as the first parameter
new Event('userLoaded');
new Event('customEvent');
new Event('πŸŽ‰');
event.js
How the Event object looks like

So how do you dispatch it? You need to call dispatchEvent on a DOM node.

Copied to clipboard!
document.getElementById('app').dispatchEvent(new Event('appReady'));
dispatch.js
Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScriptinfo Remove ads

Listening to Custom Events

We’ve successfully dispatched the event. Now it’s time to listen to it. You can listen for custom events, just like you would with standard events: using addEventListener.

Copied to clipboard! Playground
const app = document.getElementById('app');

app.addEventListener('appReady', () => console.log('The app is ready πŸŽ‰'));

// Later in the application, calling dispatchEvent
// will cause the attached eventlisteners to be executed
app.dispatchEvent(new Event('appReady'));
listen.js

Note that you can only listen to custom events fired from the same DOM element. This means that if you dispatch an event on a button in module-b.js and you listen for it on another button in module-a.js, you won’t be able to execute the callback function. Instead, to make events global, rewrite the above example in the following way:

Copied to clipboard!
// If you want to dispatch events globally, call dispatchEvent on the document
document.addEventListener('appReady', () => console.log('The app is ready πŸŽ‰'));
document.dispatchEvent(new Event('appReady'));
global.js

By attaching the event listener on the document and listening for it on the document as well, you can essentially publish and catch custom events globally, anywhere in your application.

Adding custom data

Let’s say now you also want to transfer some data between components. This can be also done using the CustomEvent constructor.

Copied to clipboard! Playground
// This will log out "The app is ready by 1597336905371 πŸŽ‰"
document.addEventListener('appReady', e => {
    console.log(`The app is ready by ${e.detail} πŸŽ‰`);  
});

// Pass an object as a second parameter with a `detail` key
document.dispatchEvent(new CustomEvent('appReady', {
    detail: +new Date()
}));
payload.js

You can also change the event propagation model by passing bubbles as another property to the payload.

Copied to clipboard! Playground
// Pass `bubbles` if you want to change the event propagation model
document.dispatchEvent(new CustomEvent('appReady', {
    bubbles: true,
    detail: +new Date()
}));
bubbles.js

Best Practices

When it comes to dispatching custom events, there’s a couple of things you should keep in mind. As you’ve noticed, you can basically create any type of event with the name of your choice. While this makes things super flexible, it also paves the way for inconsistency. Imagine the following situation:

Copied to clipboard! Playground
// You have multiple events which are expecting important params
document.addEventListener('appReady', e => console.log(`The 1st app is ready by ${e.detail} πŸŽ‰`));
document.addEventListener('appReady', e => console.log(`The 2nd app is ready by ${e.detail} πŸŽ‰`));

// Later in your app, you do the dispatching
document.dispatchEvent(new CustomEvent('appReady', {
    detail: +new Date()
}));

// The two console.logs gets printed to the console
// The 1st app is ready by 1597337145718 πŸŽ‰
// The 2nd app is ready by 1597337145719 πŸŽ‰
conflict.js

Everything works fine. As you can see, you can attach multiple event listeners with different behaviors to the same event. But what happens if you use the same event name to dispatch an event with another payload?

Copied to clipboard! Playground
// Later down in your app, a colleague of yours β€” because let's be honest, it's never our fault β€” dispatches the same event with another payload
document.dispatchEvent(new CustomEvent('appReady', {
    detail: 'This payload doesn\'t contain any timestamp πŸ€•'
}));

// The 1st app is ready by This payload doesn\'t contain any timestamp πŸ€•
// The 2nd app is ready by This payload doesn\'t contain any timestamp πŸ€•
conflict.js

As you can see, we are expecting a timestamp in the event listeners but the dispatch sends us a string. This can cause unexpected behaviors. One common way to battle this is keeping all known custom events in one place, and only allowing the use of predefined events:

Copied to clipboard! Playground
const EVENTS = {
    appReady: 'appReady',
    userLoaded: 'userLoaded',
    userSubscribed: 'userSubscribed'
};

// Only dispatch known predefined events.
document.dispatchEvent(new CustomEvent(EVENTS.appReady, { ... });
events.js

Another common practice is to namespace events by dots or colons to avoid conflict:

Copied to clipboard!
// The two events below are entirely different
// But the naming signals it clear that they belong to the same event
document.dispatchEvent(new CustomEvent('appReady.boot', { ... });
document.dispatchEvent(new CustomEvent('appReady:boot', { ... });
namespace.js

Summary

In summary, creating and dispatching custom events is fairly simple with the Event and CustomEvent interfaces. If you would like to learn more about how they work, I recommend looking through the documentation on MDN.

Did you already know about Event and CustomEvent? Let us know what are your best practices when using them in the comments! Thank you for reading through, happy coding!

5 Best Practices for Clean JavaScript
  • twitter
  • facebook
JavaScript
Did you find this page helpful?
πŸ“š More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Access 100+ interactive lessons
  • check Unlimited access to hundreds of tutorials
  • check Prepare for technical interviews
Become a Pro

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.