DEV Community

Will T.
Will T.

Posted on

Using EventTarget and CustomEvent to build a web-native event emitter

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:

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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. 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:

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;
}
Enter fullscreen mode Exit fullscreen mode
  • 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:

class CustomEventTarget extends EventTarget {}

const customEventTarget = new CustomEventTarget();
Enter fullscreen mode Exit fullscreen mode

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:

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

customEventTarget.dispatchEvent(customEvent);
Enter fullscreen mode Exit fullscreen mode

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

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

  // do whatever next here
};

customEventTarget.addEventListener('someCustomEventName', eventHandler);
Enter fullscreen mode Exit fullscreen mode

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:

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 };
};
Enter fullscreen mode Exit fullscreen mode

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

export const customPubsubA = createPubsub<{ whatever: string }>('someCustomEvent');
Enter fullscreen mode Exit fullscreen mode

And using the pubsub is also straightforward:

// 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();
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
longtrangit profile image
longtrangit

Slay :o