What is event delegation and why is it useful?
Advertisement
🧩 Scenario
Architecture Walkthrough
How Event Bubbling Powers Delegation
When a user clicks an element, the browser fires the event on the target element first and then propagates it upward through every ancestor in the DOM tree until it reaches document. This bubbling phase is what delegation relies on. Because the click eventually reaches the parent container, a single listener on the container receives every click that starts inside it.
Attaching one listener to a <ul> instead of one listener per <li> means the number of listeners stays constant no matter how many items exist. It also means items added to the list after the page loads are automatically covered without any re-attachment logic.
Identifying the Clicked Child
Inside a delegated handler, e.currentTarget is the element with the listener (the parent). e.target is the element the user actually clicked. To filter only specific children, check whether e.target matches a CSS selector using e.target.matches('.delete-btn') or whether it is contained in a matching ancestor using e.target.closest('.list-item').
closest() is the safer choice when elements have nested markup inside them. If a button contains a <span> for an icon, clicking the icon sets e.target to the span, not the button. e.target.closest('button') walks up from wherever the click landed and finds the nearest button ancestor, making the handler resilient to nested content.
Events That Do Not Bubble
Not all events bubble. focus, blur, mouseenter, and mouseleave do not bubble by default, which means a delegated listener on a parent will never fire for them. The alternatives are focusin and focusout (which do bubble) or passing { capture: true } as the third argument to addEventListener to intercept events during the capture phase before they reach the target.
Key Code Explained
const list = document.querySelector('.todo-list');
// Single delegated listener on the parent
list.addEventListener('click', (e) => {
// Handle nested elements: find the closest item regardless of what was clicked
const item = e.target.closest('.todo-item');
if (!item) return; // click landed outside any item
if (e.target.matches('.delete-btn')) {
item.remove();
return;
}
if (e.target.matches('.complete-btn')) {
item.classList.toggle('completed');
}
});
// Items added later are covered automatically — no new listener needed
function addItem(text) {
const li = document.createElement('li');
li.className = 'todo-item';
li.innerHTML = `
<span>${text}</span>
<button class="complete-btn">Done</button>
<button class="delete-btn">Delete</button>
`;
list.appendChild(li);
}
The e.target.closest('.todo-item') call is the key defensive pattern. Without it, clicking directly on the <span> text would cause e.target to be the span, the matches() checks would return false, and the handler would silently do nothing even though the user clearly clicked an item.
Tradeoffs
| Approach | Pro | Con |
|---|---|---|
| Delegation (parent) | One listener, works for dynamic elements | Requires e.target filtering; bubbling can be disrupted |
| Direct listeners | Simple, no filtering needed | Memory grows with element count; must re-attach for new elements |
| Capture phase | Works for non-bubbling events like focus | Less common, may interfere with other capture-phase handlers |
What Interviewers Actually Check
- Whether you can explain event bubbling and why it enables delegation
- Whether you know the difference between
e.targetande.currentTarget - Whether you handle nested HTML with
closest()rather than justmatches() - Whether you can name events that do not bubble and know the alternatives
- Whether you understand when delegation adds complexity rather than reducing it
Follow-Up Questions
- How would you implement delegation for
focusevents on inputs inside a form? - If a child element calls
e.stopPropagation(), how does this affect a delegated listener on the parent? - How does React's synthetic event system implement delegation internally, and how did this change in React 17?
- What is the difference between attaching a listener in the bubble phase vs the capture phase?
- When would you choose direct listeners over delegation even for a large list?
Common Candidate Mistakes
- Checking
e.target.matches()without accounting for nested HTML inside child elements, causing the handler to miss clicks on inner spans or icons - Confusing
e.target(the element clicked) withe.currentTarget(the element with the listener) - Not knowing that
focusandblurdo not bubble, then being surprised when a delegated focus handler never fires - Forgetting that
e.stopPropagation()inside any child prevents the event from reaching a delegated ancestor listener - Applying delegation to every list regardless of whether elements are dynamic, adding unnecessary complexity for static content
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you explain the bubbling phase and why it enables delegation?
- Can you write a delegated click handler using
e.target.closest()to handle nested content correctly? - Can you explain the difference between
e.targetande.currentTarget? - Can you name two events that do not bubble and describe the workaround for each?
- Can you explain what
e.stopPropagation()inside a child does to a delegated ancestor listener?
Summary
Event delegation places a single event listener on a parent element and uses the bubbling phase to intercept events from any descendant. The handler identifies which child triggered the event using e.target and filters by CSS selector with matches() or closest(). This reduces the total number of listeners in the DOM, works automatically for dynamically added elements, and simplifies cleanup since only the parent listener needs to be removed.
The practical complexity of delegation lies in handling nested HTML inside the target elements. Using e.target.closest(selector) instead of e.target.matches(selector) handles this correctly by walking up the DOM from wherever the click landed.
Delegation breaks when a child handler calls e.stopPropagation() and cannot be used directly with non-bubbling events like focus and blur. Those events require focusin, focusout, or the capture phase as alternatives.
Does event delegation work for all events?
It works for events that bubble (like click). Some events do not bubble, so you cannot delegate those directly.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement