Suggest an editImprove this articleRefine the answer for “Event delegation”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Event delegation** is a pattern that attaches one listener to a parent element to handle events from all its children via bubbling. ```javascript document.querySelector('ul').addEventListener('click', (e) => { if (e.target.matches('li')) console.log(e.target.textContent); }); ``` **Key point:** the listener works for dynamically added children without rebinding.Shown above the full answer for quick recall.Answer (EN)Image**Event delegation** is a pattern where you attach one event listener to a parent element and let event bubbling carry clicks from any child up to that single handler. ## Theory ### TL;DR - Think of it like a hotel front desk: guests from any room walk up to one spot instead of each room having its own staff. - Main difference: one parent listener vs. one listener per child. The parent version survives DOM changes without rebinding. - `event.target` is the clicked element; `event.currentTarget` is always the parent with the listener. - Use when children are dynamic (API-loaded lists, search results) or numerous (50+). - For 3-5 static items that never change, direct listeners are simpler. ### Quick example ```html <ul id="menu"> <li data-action="home">Home</li> <li data-action="about">About</li> </ul> ``` ```javascript // Without delegation - breaks when list changes // document.querySelectorAll('li').forEach(li => li.addEventListener('click', handler)); // With delegation - works for any li, present or future document.getElementById('menu').addEventListener('click', (e) => { if (e.target.matches('li')) { console.log(e.target.dataset.action); // "home" or "about" } }); ``` Add a new `<li data-action="contact">Contact</li>` at runtime and the handler still fires. No rebinding. ### Key difference Direct listeners attach to each child at the moment you call `addEventListener`. Add a new child later and it has no listener. Delegation flips this: the parent is already listening, and new children bubble events up to it automatically. The cost is one check per click via `matches()` or `closest()`, but that is never the bottleneck. ### When to use - Dynamic lists (search results, infinite scroll, todo items from an API) → delegate to the container. - More than 50 similar children → one listener beats dozens. - Mobile or performance-sensitive UI → fewer listeners means less memory pressure. - 3-5 static items that never change → direct listeners are cleaner. - Shadow DOM or custom elements → check first, events don't cross shadow boundaries by default. ### How it works internally Browsers dispatch events in three phases: capture (root to target), target, then bubbling (target back to root). Delegation runs in the bubbling phase, which is the default. When a user clicks a `<li>`, the event hits that element, then travels up through `<ul>`, `<body>`, `<html>`, `document`. Your parent listener intercepts it during that trip. The browser gives you `event.target` (the original clicked element) and `event.currentTarget` (the element where your listener lives). That distinction is what makes the whole pattern work. I've seen this fix a real memory leak in a chat app that was re-attaching listeners to message rows on every WebSocket update. One delegated listener on the list container was the entire fix. ### Common mistakes **Forgetting to filter by element type:** ```javascript // Wrong - fires on the ul itself or any nested span/img inside li parent.addEventListener('click', (e) => console.log(e.target.textContent)); // Right - check for the exact element parent.addEventListener('click', (e) => { const li = e.target.closest('li'); if (li) console.log(li.textContent); }); ``` `closest()` is safer than `matches()` alone when your `<li>` contains icons or `<span>` tags. **Confusing `target` with `currentTarget`:** ```javascript parent.addEventListener('click', (e) => { console.log(e.currentTarget); // Always the parent - not what you want console.log(e.target); // The actual clicked element - this is it }); ``` **A child calling `stopPropagation()`:** ```javascript // Somewhere in your code: child.addEventListener('click', (e) => e.stopPropagation()); // Kills bubbling // Your parent listener never fires now parent.addEventListener('click', handler); // Nothing happens ``` If you control both listeners, avoid `stopPropagation()`. If you don't, check `e.defaultPrevented` as a signal. **Delegating on `document` without scoping:** ```javascript // Catches every click on the page - fragile document.addEventListener('click', (e) => { if (e.target.tagName === 'LI') doSomething(); }); // Better - scope to a specific container document.getElementById('menu').addEventListener('click', (e) => { if (e.target.matches('li')) doSomething(); }); ``` **Using delegation with non-bubbling events:** `focus`, `blur`, and `scroll` don't bubble. Use `focusin`/`focusout` instead (they do bubble), or pass `{capture: true}` to the listener. ### Real-world usage - React: attaches one synthetic event listener to the root container, not to each component separately. - jQuery: `$(parent).on('click', '.child', handler)` has built-in delegation since v1.7. - Vue 3: `v-on` on a container with `event.target.closest()` for dynamic `v-for` lists. - Alpine.js and other lightweight frameworks use this as a core pattern to stay lean. ### Follow-up questions **Q:** What is the difference between `event.target` and `event.currentTarget`? **A:** `target` is the element the user actually clicked. `currentTarget` is the element where the listener is attached. In delegation, `currentTarget` is always the parent; `target` is the child you care about. **Q:** How does delegation handle dynamically added elements? **A:** Automatically. The parent listener was attached once and stays. New children bubble events up to it without any rebinding. **Q:** Which events don't bubble and can't use standard delegation? **A:** `focus`, `blur`, `scroll`, `mouseenter`, `mouseleave`. Use `focusin`/`focusout` as bubbling alternatives, or `{capture: true}` for the others. **Q:** What happens if a child calls `stopPropagation()`? **A:** Bubbling stops at that child and the parent listener never fires. This is the most common reason delegation quietly breaks in large codebases. **Q:** How does delegation work in Shadow DOM? (senior-level) **A:** Events don't cross shadow boundaries by default. Use `event.composedPath()` to inspect the full path, or dispatch custom events with `{bubbles: true, composed: true}` to pierce them. ## Examples ### Basic: menu with data attributes ```html <ul id="nav"> <li data-page="home">Home</li> <li data-page="about">About</li> <li data-page="contact">Contact</li> </ul> ``` ```javascript document.getElementById('nav').addEventListener('click', (e) => { const item = e.target.closest('li'); if (!item) return; console.log('Navigate to:', item.dataset.page); // Output: "Navigate to: home" when Home is clicked }); ``` `closest()` walks up from the clicked element until it finds a `<li>`. This handles clicks on a nested icon or `<span>` inside the list item without any extra logic. ### Intermediate: dynamic todo list ```javascript function addTodo(text) { const li = document.createElement('li'); li.textContent = text; li.dataset.done = 'false'; document.getElementById('todos').appendChild(li); } // One listener handles all todos, including ones added after page load document.getElementById('todos').addEventListener('click', (e) => { const todo = e.target.closest('li'); if (!todo) return; const isDone = todo.dataset.done === 'true'; todo.dataset.done = String(!isDone); todo.style.textDecoration = isDone ? 'none' : 'line-through'; console.log('Toggled:', todo.textContent); }); addTodo('Buy groceries'); addTodo('Write tests'); // Click either item - both toggle correctly, no re-registration needed ``` The `addTodo` function keeps adding items. The listener never changes. That's the whole point. ### Advanced: multiple actions from one container ```javascript // Each button declares its action via data-action const actions = { delete: (el) => el.closest('li').remove(), edit: (el) => { const li = el.closest('li'); const text = prompt('Edit:', li.dataset.text); if (text) li.dataset.text = text; }, complete: (el) => el.closest('li').classList.toggle('done'), }; document.getElementById('list').addEventListener('click', (e) => { const btn = e.target.closest('[data-action]'); if (!btn) return; const action = btn.dataset.action; if (actions[action]) actions[action](btn); // One listener routes to the right handler based on data attribute }); ``` Adding a new action means adding one entry to the `actions` object. No new listeners, no extra bindings. This pattern scales cleanly across large lists.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.