DOMintermediate
Event Delegation Pattern
Attaching a single event listener to a common ancestor instead of many listeners on individual children, relying on event bubbling.
One listener on a parent handles events from any of its children, using bubbling.
Event delegation is like a single receptionist at a building's front desk handling every visitor's request instead of stationing a separate employee at every single office door — when someone shows up, the receptionist just checks which office they're actually asking about (event.target.closest) rather than needing dedicated staff hired and fired every time an office opens or closes.
Key Concepts
1
// instead of a listener per row
list.addEventListener('click', (e) => {
const btn = e.target.closest('.delete');
if (!btn) return;
remove(btn.dataset.id);
});2
Three reasons it is the default approach.One listener instead of many. A thousand rows is one handler, not a thousand — less memory and a much
faster initial render.
3
It covers elements that do not exist yet. Rows added later are handled with no re-binding. That is the
main reason: dynamic lists otherwise need a listener attached on every update.
4
No cleanup per element. Removing a row cannot leave an orphaned listener holding it in memory.
5
closest is what makes it robust.
e.target might be an icon INSIDE the button
e.target.closest('.delete') walks up to the buttonclosest
6
Checking e.target.matches('.delete') fails when the click lands on a nested span, which is the usual bug
in hand-written delegation.
e.target.matches('.delete')
7
Handling several actions with one listener.
list.addEventListener('click', (e) => {
const el = e.target.closest('[data-action]');
if (!el || !list.contains(el)) return;8
switch (el.dataset.action) {
case 'edit': edit(el.dataset.id); break;
case 'delete': remove(el.dataset.id); break;
}
});9
The markup declares what each control does, and the handler dispatches.
10
The contains check matters when the container is document — without it, a click anywhere on the page
matching that selector is handled.
containsdocument
11
Where it does not work. Events that do not bubble — focus, blur, mouseenter. Use focusin,
focusout and mouseover instead.
focusblurmouseenterfocusinfocusout
12
And the trade to acknowledge. Every matching click runs your handler, including ones you ignore, and the
logic is one step removed from the element it concerns. For a small fixed set of controls, direct listeners
are clearer. For lists and anything dynamic, delegation is the right default.
13
What the interviewer is probing.1. "What problem does delegation solve?" Probing: the motivation. Stalls: "Fewer listeners."
Moves up: one listener on a parent handles current and future children, so dynamically added rows
need no wiring and memory stays flat.
14
2. "How do you identify which child was clicked?" Probing: the mechanism. Stalls:
"event.target." Moves up: event.target.closest(selector), which handles a click landing on a
nested element inside the row.
15
3. "What is the difference between target and currentTarget?" Probing: the two references.
Stalls: "The same." Moves up: target is what was clicked; currentTarget is the element the
handler is attached to.
16
4. "When does delegation not work?" Probing: the limits. Stalls: "Always works." Moves up:
for events that do not bubble, and when a child calls stopPropagation.