When do event listeners need to be manually removed?

I’ve been using Observable for quite a while, but one thing I’m not clear on is when event listeners need to be manually removed.

For example, right now I have a button that does something when clicked, and the cell looks like this:

{
  const btn = Inputs.button("Do thing");

  const controller = new AbortController;
  invalidation.then(() => controller.abort());
  
  btn.addEventListener('click', () => {
    // does thing
  }, { signal: controller.signal });

  return btn;
}

Do I need to bother with the AbortController? Or will the event listener automatically be destroyed if the cell is re-run? Or does it not matter because the original button element will be gone anyway?

What’s the best practice here?

In this case you don’t need to bother, because the element is “owned” by the cell and only attached to the cell’s output container. When the cell invalidates, btn gets removed and garbage collected.

You only need to clean up listeners if they are attached to event targets that persist past the cell’s lifetime. Examples are window or DOM elements in other cells.


As an aside, I wonder if we could provide a signal variable to cells in addition to invalidation, so that removing listeners and aborting fetch requests becomes easier.

In the meantime, here’s a copy-pastable snippet:

function toSignal(invalidation) {
  const controller = new AbortController;
  invalidation.then(() => controller.abort());
  return controller.signal;
}

and usage:

{
  const signal = toSignal(invalidation);
  const d = Date.now();
  window.addEventListener("resize", () => {
    console.log("resized", d);
  }, {signal});
}
data = fetch("https://example.com/data.csv", {
  signal: toSignal(invalidation)
}).then(r => r.text())
2 Likes