TypeScript & JavaScript · T259

Why does a closed panel still react?

Removing a panel does not remove its window listener. The callback can keep the old panel reachable and react after it closes.

The important bit
Abort the listener on close or remove the same function with the same capture setting. This removes one reference path; other references may remain.

Understand it. Then fix it.

The panel left. Its listener stayed.

Each opening adds a new window listener. Removing the panel does not remove that listener. Closing twice can leave two callbacks waiting for the next resize.

Still reachable. Still kept.

The window keeps the callback. The callback keeps the panel it uses. Garbage collection frees unreachable objects. This panel is still reachable, even though you cannot see it.

One signal. One cleanup.

Create an AbortController for each opening and pass its signal when adding the listener. On close, abort removes that listener. Then remove the panel.

const c = new AbortController();
window.addEventListener(
  "resize", () => layout(panel),
  { signal: c.signal }
);
return () => {
  c.abort();
  panel.remove();
};

Or keep the same function.

You can also remove the listener directly. Keep the same function reference and the same capture setting. A new arrow function will not match the old listener.

const onResize = () => layout(panel);
window.addEventListener(
  "resize", onResize
);
// On close:
window.removeEventListener(
  "resize", onResize
);

Close. Reopen. One active callback.

Repeat the same steps after cleanup. Now only the open panel reacts. This removes one reference path; other references may still keep the old panel in memory.

Code blocks are teaching excerpts. Keep the surrounding error handling and application requirements.

Save the code excerpts ↓
Read the full transcript

I close this panel, open it again, and resize the window. Why does the same work run twice? Each opening adds a new window listener. Removing the panel does not remove that listener. Closing twice can leave two callbacks waiting for the next resize. The window keeps the callback. The callback keeps the panel it uses. Garbage collection frees unreachable objects. This panel is still reachable, even though you cannot see it. So I need to remove the listener when I close the panel. How? Create an AbortController for each opening and pass its signal when adding the listener. On close, abort removes that listener. Then remove the panel. You can also remove the listener directly. Keep the same function reference and the same capture setting. A new arrow function will not match the old listener. Repeat the same steps after cleanup. Now only the open panel reacts. This removes one reference path; other references may still keep the old panel in memory. I deleted the panel, but kept paying it to work. I accidentally built middle management.

Go to the source