javascript 32 lines · 8 steps

Event delegation with a clean teardown

A single listener on a root element handles events for many matching descendants, with an unsubscribe function for cleanup.

Explained by highlit
1function delegate(root, eventType, selector, handler) {
2 const listener = (event) => {
3 let node = event.target;
4 while (node && node !== root) {
5 if (node.matches(selector)) {
6 handler.call(node, event, node);
7 return;
8 }
9 node = node.parentNode;
10 }
11 };
12 root.addEventListener(eventType, listener);
13 return () => root.removeEventListener(eventType, listener);
14}
15 
16class EventDelegator {
17 constructor(root) {
18 this.root = root;
19 this.cleanups = [];
20 }
21 
22 on(eventType, selector, handler) {
23 const off = delegate(this.root, eventType, selector, handler);
24 this.cleanups.push(off);
25 return this;
26 }
27 
28 destroy() {
29 this.cleanups.forEach((off) => off());
30 this.cleanups = [];
31 }
32}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1One listener on a shared ancestor can serve many children, so you avoid attaching (and leaking) handlers on every element.
  2. 2Returning an unsubscribe closure makes teardown symmetric with setup and keeps the listener reference in scope.
  3. 3Collecting cleanup functions lets a coordinator dispose of all its listeners in one call.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Event delegation with a clean teardown — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code