Event emitters

EventTarget with CustomEvent

Baseline widely available

An application event bus is usually a small class with on, off and emit. EventTarget is that class, already in the platform: make a bare new EventTarget(), or extend it, then dispatch a CustomEvent and listen with addEventListener. Passing an AbortSignal to addEventListener takes a whole group of listeners off in one abort() call, which is the part hand-rolled emitters usually leave to the caller.

When this applies

Passing messages between parts of an app through a small event bus.

The native approach

const bus = new EventTarget();

const controller = new AbortController();
bus.addEventListener(
  "cart:add",
  (event) => render(event.detail.sku),
  { signal: controller.signal },
);

bus.dispatchEvent(new CustomEvent("cart:add", { detail: { sku: "A1" } }));

// Removes every listener registered with this signal.
controller.abort();

MDN reference

When the dependency is still right

An answer that always says "the platform covers it" is worse than no answer. These are the cases where this one does not hold.

  • Your emit calls pass several arguments. A CustomEvent carries one detail property, so each emit has to become an object and each handler reads event.detail.
  • You use wildcard listeners or read the registered listener list, as mitt's all map allows. EventTarget exposes neither.
  • You need the bus in Node below 15.0.0, where EventTarget is not a global. Node's own EventEmitter covers that without a dependency.
  • You depend on the library's dispatch ordering or on re-entrant emits behaving a particular way, which DOM event dispatch does not promise to match.
  • Your support target reaches below Safari 14, where new EventTarget() cannot be constructed, or below Safari 15, where addEventListener ignores the signal option and listeners never come off.
  • The package is also pulled in transitively by something else, so dropping your direct dependency does not remove the bytes from the bundle.

Packages this covers