Event delegation: one listener for a thousand items
The N-listener anti-pattern
document.querySelectorAll(".item").forEach((el) => {
el.addEventListener("click", () => { /* ... */ });
});Works fine for 5 items. With 5,000 items it's 5,000 listeners β memory bloat + slow first paint. Worse: dynamically-added items don't get the listener and silently break.
Delegation: one listener on the parent
list.addEventListener("click", (event) => {
const item = event.target.closest(".item");
if (!item) return;
// handle the click on .item β works for items added later too
});Events bubble. A click on a child <li> bubbles up to its parent <ul>. One listener on the parent catches every click on any .item, present or future.
Why event.target.closest
event.target is the exact element clicked β might be a deeper <span> inside the item. .closest(".item") walks up until it finds the nearest matching ancestor (or returns null). That's how you distinguish "user clicked something inside an item" vs "user clicked outside any item."
React et al. do this for you
Every modern framework uses delegation at the document root for performance. You don't think about it in React. Knowing the underlying pattern is what makes the framework feel obvious instead of magic.
Sign up to start coding
Theory is open to everyone. The interactive editor, live preview, and check are unlocked with a 7-day free trial β card required, cancel anytime.
Sign up β free trial βFirst 10 lessons in each track are free. No card needed for those.