Introduction

Custom event is the special class in JavaScript that allows you to define an event with a custom name and dispatch it at any time when you need it. The best thing about it, is that you can pass any arguments to the event and get them later in your app after the event has been fired.

Custom events in JavaScript are not a silver bullet solution for all the problems, you might reach for them only for specific cases. One of these cases I will show you in this article.

If you ask me what is the best thing about custom events, I would say the decoupling of your code from the framework or specific module you are using. But, be careful, if you use too many custom events in your code base you can end up with spaghetti code, and it will be difficult to maintain it.

Event thought, they really help you to follow the Single Responsibility Principle, they might also break this rule in some cases by introducing a new coupling in your system that didn’t exist before. We will discuss this in the example I will give you today.

And I know talking about SOLID principles in a JavaScript article is weird, but I promise we are going to keep it technical as much as possible.

If you are interested in learning JavaScript or other programming languages, visit Laracasts or other good resources for learning real programming.

Dispatch and listen for events

I'm sure you know how to listen for events in JavaScript but still, here is the example of how we can do it on a window object:

window.addEventListener('postSaved', () => console.log('Post has been saved'))

Defining and dispatching custom events is pretty straightforward:

window.dispatchEvent(new CustomEvent('postSaved'))

That's it. That's probably all you need to know about custom events in JavaScript. First, we're listening for the postSaved event. After we saved the post, our event was dispatched, and we got a message Post has been saved on our console.

Passing arguments

The CustomEvent constructor takes an object as the second argument. That's what we are going to use for passing our data.

window.dispatchEvent(new CustomEvent('postSaved', {
    detail: 'Post has been saved',
}))

detail is a special property that is used for passing data. In our case, we are passing a string, but it can be any data. Here is what we are going to write to get our message in the event listener callback:

window.addEventListener('postSaved', e => console.log(e.detail))

Other than a detail property, you can also pass 2 more:

Property name Property type Description
detail any You can assign any data to the detail property and get it back later in the event listener callback.
bubbles boolean Indicates whether an event is a bubbling event. If it is, it will bubble up (like a bubble in water) to the object's parent. If you dispatch an event on a window object, like in our example, you don't need to worry about bubbles. It only matters if the element is inside the DOM.
cancelable boolean Indicates whether the event can be canceled, and therefore prevented as if the event never happened.

My use case

There were many places in my JavaScript projects where I felt like I'm missing something. I'm sure everyone must have felt this way. It's when you write some lines of code, and you sense that there is a better way to do this. That is what was happening to me when I wasn't aware of Custom events.

My use cases are usually all around the promises. Make a request, wait for the result, when the result comes back from the server, dispatch an event. But, it depends on the project.

Here is the screenshot of the project that I did:

Timetable project screenshot

It's only a part of the whole project, I just want to give you an idea how many buttons and select elements it has. Here is the full page if you're interested. As you can see, there are 8 sections. The biggest is the timetable, and others are just sections with data that depends on the timetable.

When you click any button or select any element, each section has to be refreshed and populated with new data. The main point that I want to make is that every section depends on each other. Unfortunately, I wasn't aware of Custom events when I was working on the project.

For each section, I have a class called Listener, each Listener is responsible for listening to events, and submitting new data to the server when the event has happened. This is how listeners look like:

export default class extends Handler {
    // ...

    handleSavingData(form) {
        // ...

        new Request({ params: params, method: 'saveWorkTime' })
            .send()
            .then(res => this.handleResponse(res.data))
            .catch(err => console.error(err))
            .finally(() => this.showButtonSpinner(false))
    }
}

Each of them has a handleSavingData method that saves data and calls the handleResponse method.

    handleResponse(response) {
        this.handleError(response)
        this.refreshContent()
    }

    refreshContent() {
        new WorkTimeLoader().load()
        new StatsForMonthLoader().load()
        new MustWorkTimeLoader().load()
        new StatsForYearLoader().load()
        new VacationTableLoader().load()
    }

refreshContent instantiates 5 classes and calls the load method on each of them. It feels dirty to do that. Because these 5 classes that are being instantiated have nothing to do with the listener. I'm a little worried about Separation of concerns here. My Listener classes have nothing to do with the Loader classes. They should not be here.

To make it even worse, I have the refreshContent method in each listener. You might say that it would be nice to extract these 5 lines into a function and call it here. But as you can see, that's precisely what I did. The other thing is that not every listener calls 5 Loader classes, some of them call 3 or 4. I think that is a perfect place for Custom events.

Refactoring to Custom events

Of course, there are other ways to improve this code, this is just one way of doing it. Let's dispatch a custom event in the refreshContent method instead of instantiating 5 classes.

import SECTIONS from './sections'

export default class extends Handler {
    //...

    refreshContent() {
        window.dispatchEvent(new CustomEvent('refreshContent', {
            detail: [
                SECTIONS.WORK_TIME,
                SECTIONS.STATS_FOR_MONTH,
                SECTIONS.MUST_WORK_TIME,
                SECTIONS.STATS_FOR_YEAR,
                SECTIONS.VACATION_TABLE,
            ],
        }))
    }
}

I need these section names later, remember that some of my classes don't need to refresh all sections, some of them need to refresh 3 or 4. I didn't use the hard-coded strings with names, I prefer always use constant variables extracted into a separate file.

I always create constants for custom event names as well. For simplicity, I'm going to keep the names with strings in this blog post.

// sections.js
export default {
    WORK_TIME: 0,
    STATS_FOR_MONTH: 1,
    MUST_WORK_TIME: 2,
    STATS_FOR_YEAR: 3,
    VACATION_TABLE: 4,
}

Now, I can use these constants anywhere in my code and get intellisense for free. I can forget about misspelling a section name or forgetting the name of it.

Vscode intellisense for extracted object

It's time to listen for this event. Create a separate file refreshTablesListener.js. This file should be executed in the main entry file, which is executed when the page is loaded.

window.addEventListener('refreshContent', e => handleRefreshContentEvent(e.details))

function handleRefreshContentEvent(sectionsNames) {
    const sections = {
        [SECTIONS.WORK_TIME]: new WorkTimeListener(),
        [SECTIONS.STATS_FOR_MONTH]: new StatsForMonthLoader(),
        [SECTIONS.MUST_WORK_TIME]: new MustWorkTimeLoader(),
        [SECTIONS.STATS_FOR_YEAR]: new StatsForYearLoader(),
        [SECTIONS.VACATION_TABLE]: new VacationTableLoader(),
    }

    for (const sectionName in sections)
        if (sectionsNames.includes(parseInt(sectionName)))
            sections[sectionName].load()
}

This code looks a bit complicated at first glance, but it's a lot simpler than it seems. We've created a function which will be called when our page is loaded. It will basically create an event listener with the name refreshContent, when you dispatch this event, it will execute loader classes that we need.

This is an example of executing on only 2 loaders:

window.dispatchEvent(new CustomEvent('refreshContent', {
    detail: [
        SECTIONS.WORK_TIME,
        SECTIONS.STATS_FOR_MONTH,
    ],
}))

Be careful with this approach. You might get into an infinite loop of event calls. It's important that these functions don't fire refreshContent event themselves. You aim to avoid creating an uncontrolled infinite loop in your system.

The simplest way to avoid it, is by having separate functions for fetching data from the server.

2 JavaScript methods for fetching data from the server

You better have one method for fetching the data and the second method for refreshing the data. The only difference between them is the event dispatcher, everything else is just about the same.

Tip for TypeScript devs

I usually wrap the dispatching logic and listening logic into 2 functions: dispatchEvent and listenEvent.

// dispatchEvent.ts

export default <T>(name: string, params?: T) => {
    const event = new CustomEvent(name, { detail: params })
    window.dispatchEvent(event)
}
// listenEvent.ts

export default <T>(name: string, callback: (data: T) => void) => {
    window.addEventListener(name, (e: any) => callback(e.detail))
}

It adds more convenience in my opinion, especially with TypeScript because I can now tell the TypeScript Transpiler what type I'm expecting when listening to the event. For example, when I'm listening to the event which will give me the object with first and last name, I can define listener like this:

type UserUpdatedListenerData = {
    first: string
    last: string
}

listenEvent<UserUpdatedListenerData>(events.USER_UPDATED, name => {
    form.first = name.first
    form.last = name.last
})

Conclusion

After the refactoring, my Listener classes don't know anything about Loader classes. Separation of concerns is the main goal of refactoring it to Custom events, and I'm happy with the result.

Custom events could be extremely beneficial in JavaScript heavy apps. There is no need to use them in every then method in promises, but I don't see any downsides of using them. After I've discovered it, I use it when I have an urge to do so.

But remember that it is best to use an event for one specific purpose to avoid breaking other responsibilities that are outside your event. Don't create event handlers which need to be reused for different purposes. This way you will end up with a mess in your code base, and you will have difficulty maintaining it in the future.

Moreover, the other thing which can be dangerous is having an infinite loop of events. It might happen when you are dispatching an event which triggers the same event that was triggered before that. So, watch out for that one.

And lastly, be careful when using custom events in legacy projects. Choose event names more careful. I've used short names like refreshContent, but in the real project I would extract these names into constants, and I would assign those constants to random strings like LSfIbPSJqPZZJQywPFeu. Look at the example:

export default {
    events: {
        REFRESH_CONTENT: 'LSfIbPSJqPZZJQywPFeu',
    },
}

Anyway, thanks for your time reading this post. I hope that you learned something new today. Don't forget to share this post if you like it.

❤️ Thanks to Jexo for the beautiful photo for this post.

Keywords: js, frontend