DEV Community

Discussion on: Type-Safe CustomEvents: Better Messaging with Native APIs

Collapse
 
trinhcuong-ast profile image
Kai Alder

This is really neat. I've been doing something similar but way less elegant — basically just a plain object with event name keys mapping to callback arrays, which obviously gives you zero type safety.

The dispatchEvent overload with the conditional tuple type for void events is a nice touch. I always end up with awkward undefined params when an event doesn't carry data.

One thing I'm wondering — have you run into issues with memory leaks when listeners aren't properly cleaned up? With the native EventTarget you don't get a once equivalent on removeEventListener, so I've had to be pretty disciplined about cleanup in SPAs. Do you add any kind of dispose pattern on top of this?

Collapse
 
link2twenty profile image
Andrew Bone • Edited

For removing event listeners you can either use the native once, this will disconnect the event listener once it's been caught once.

// set the event listener to once
shoppingCartSingleton.addEventListener('item-added', (event) => { ... }, {
  once: true
});
Enter fullscreen mode Exit fullscreen mode

Or, my preferred method, you can send an abort signal to remove as many event listeners as you like.

// declare an abort controller
const ac = new AbortController();

// pass the signal through to the event listener
shoppingCartSingleton.addEventListener('item-added', (event) => { ... }, {
  signal: ac.signal
});

// abort the AbortController to remove the event listener
ac.abort();
Enter fullscreen mode Exit fullscreen mode

For example in react you would do something like this

useEffect(() => {
  const ac = new AbortController();

  // attach 3 listeners all with the same signal
  shoppingCartSingleton.addEventListener('item-added', (event) => { ... }, { signal: ac.signal });
  shoppingCartSingleton.addEventListener('item-removed', (event) => { ... }, { signal: ac.signal });
  shoppingCartSingleton.addEventListener('cart-cleared', (event) => { ... }, { signal: ac.signal });

  // on unmount, all 3 listeners are removed.
  return () => ac.abort();
}, [])
Enter fullscreen mode Exit fullscreen mode