Using EventTarget and CustomEvent to build a web-native event emitter
Published: 2023-11-04 03:40:18

Web development often involves creating dynamic and interactive user interfaces. To achieve this, developers rely on various design patterns and JavaScript features to handle events and user interactions effectively. One such example is the Publisher-Subscriber pattern that can easily be implemented by using the `EventTarget` interface.

In this article, we'll explore how to use the `EventTarget` interface to easily and properly implement the Publisher-Subscriber pattern to handle events in the Web environment.

## Wait... what exists before `EventTarget`?

Before `EventTarget` came to the browser land, when one needed to implement the Publisher-Subscriber pattern, they would probably have to build everything from scratch. A common code example is to create a `PubSub` class that manages its internal events like this:

```ts
type EventCallback<T> = (data: T) => void;

class PubSub<T> {
  #events: Record<string, EventCallback<T>[]> = {};

  addEventListener(event: string, callback: EventCallback<T>): void {
    if (!this.#events[event]) {
      this.#events[event] = [];
    }

    this.#events[event].push(callback);
  }

  dispatchEvent(event: string, data: T): void {
    const eventCallbacks = this.#events[event];

    if (eventCallbacks && eventCallbacks.length > 0) {
      eventCallbacks.forEach((callback) => {
        callback(data);
      });
    }
  }

  removeEventListener(event: string, callback: EventCallback<T>): void {
    const eventCallbacks = this.#events[event];

    if (eventCallbacks && eventCallbacks.length > 0) {
      this.#events[event] = eventCallbacks.filter((cb) => cb !== callback);
    }
  }
}
```

However, that would be a little bit cumbersome, and perhaps each programmer will likely implement it slightly differently, which can be error-prone.

## Introducing `EventTarget`

`EventTarget` is now [supported by literally every browser](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget). The `EventTarget` interface represents an object that can receive events and have listeners for those events. Most DOM elements in a web page, such as buttons, forms, and input fields, implement the EventTarget interface. This allows developers to add event listeners to these elements and respond to user interactions, like clicks or keypresses. However, what's even cooler is that you can use it separately without attaching it to any DOM element.

In TypeScript, the EventTarget interface is defined as follows:

```ts
interface EventTarget {
  addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean): void;
  dispatchEvent(event: Event): boolean;
  removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
}
```

- `addEventListener`: Appends an event listener for events with the equivalent type, which can be any arbitrary name or pre-defined types like `click`, `keydown`, etc. The callback argument sets the callback that will be invoked when the event is dispatched.
- `dispatchEvent`: Dispatches a custom event on the target element.
- `removeEventListener`: Removes the event listener in target's event listener list with the same `type`, `callback`, and `options`.

## How `EventTarget` and `CustomEvent` work together

You can always create a custom `EventTarget` of your own by simply creating a class that extends `EventTarget` and then instantiate it:

```ts
class CustomEventTarget extends EventTarget {}

const customEventTarget = new CustomEventTarget();
```

This (`customEventTarget`) acts as a bridging component (a.k.a. a broker) where the data exchange happens. And with `CustomEvent`, we can include additional data however we want through its `detail` property:

```ts
const data = { whatever: 'you want' };
const customEvent = new CustomEvent('someCustomEventName', { detail: data });

customEventTarget.dispatchEvent(customEvent);
```

And then we can easily get that data in the event handler:

```ts
const eventHandler = (event) => {
  const data = event.detail;

  // do whatever next here
};

customEventTarget.addEventListener('someCustomEventName', eventHandler);
```

And don't forget to remove the event listener using `removeEventListener`.

## Make it less verbose and easily reusable

Creating a new event may seem like a hassle sometimes. Therefore, we can have this small piece of code to reduce the boilerplate:

```ts
class CustomEventTarget extends EventTarget {}

export const createPubsub = <T = Record<string, unknown>>(eventName: string) => {
  const customEventTarget = new CustomEventTarget();

  const subscribe = (eventHandler: (data: T) => void) => {
    const scriptContentResizedEventHandler = (event: Event) => {
      const data = (event as CustomEvent).detail as T;

      eventHandler(data);
    };

    customEventTarget.addEventListener(eventName, scriptContentResizedEventHandler);

    return () => {
      customEventTarget.removeEventListener(eventName, scriptContentResizedEventHandler);
    };
  };

  const publish = (data: T) => {
    customEventTarget.dispatchEvent(new CustomEvent(eventName, { detail: data }));
  };

  return { subscribe, publish };
};
```

Now it only takes one line of code whenever we want to create a new pubsub:

```ts
export const customPubsubA = createPubsub<{ whatever: string }>('someCustomEvent');
```

And using the pubsub is also straightforward:

```ts
// Dispatch an event with some custom data
customPubsubA.publish({ whatever: 'you want' });

// To subscribe to the event:
const unsubscribe = customPubsubA.subscribe(({ whatever }) => {
  // do whatever here
});

// ...and remember to unsubscribe when it is no longer needed
unsubscribe();
```

## Conclusion

The simple yet powerful combination makes it a valuable tool in various scenarios. In some ReactJS projects, this pattern can often be used to avoid having to rely on unnecessary and complex state management or prop drilling.

In summary, the use of the `EventTarget` interface and `CustomEvent` provides a "standardized" way to manage events and listeners. It has greatly simplified the implementation of the Publisher-Subscriber pattern in web development.