← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

Event Delegation

javascript ES5 Intermediate
debt(d7/e3/b3/t5)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). ESLint can flag some patterns (listeners in loops) and Chrome DevTools can show listener counts, but the core misuses — attaching listeners to every list item, broken handlers on dynamic elements, memory leaks from many individual listeners — typically only surface through runtime profiling or user-reported bugs on large lists. No linter rule catches this by default; it requires specialist inspection.

e3 Effort Remediation debt — work required to fix once spotted

Closest to 'simple parameterised fix' (e3). The quick_fix states: attach one listener to a common parent and use event.target (or event.target.closest()) to identify the child. This is a small, contained refactor — replace N per-item addEventListener calls with one on a parent — typically within one component or template, not cross-cutting.

b3 Burden Structural debt — long-term weight of choosing wrong

Closest to 'localised tax' (b3). The applies_to scope is web contexts only (DOM-based UIs), and the choice is localised to UI components that render lists or dynamic content. Getting it wrong imposes a memory/performance tax on that component, but the rest of the codebase is largely unaffected unless the pattern is systemic across many list components.

t5 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'notable trap' (t5). The misconception field identifies the trap: developers treat delegation as optional optimisation, but for dynamic lists it is mandatory. The common_mistakes reinforce additional traps: using event.target instead of event.target.closest() causes wrong-target bugs when children are clicked, and delegating too broadly to document/body is a documented gotcha. These are well-known but non-obvious pitfalls that most developers hit at least once.

About DEBT scoring →

Also Known As

event bubbling delegated events

TL;DR

A pattern that attaches a single event listener to a parent element instead of many listeners to children — using event bubbling to handle events from dynamically added children.

Explanation

DOM events bubble up from the target element through its ancestors. Event delegation exploits this: instead of attaching listeners to every list item, attach one listener to the list. The listener uses event.target to identify which child was clicked. Benefits: fewer event listeners (memory), works for dynamically added elements, and simplifies code. The pattern is essential for rendering lists where items are added and removed frequently.

Diagram

flowchart TD
    subgraph Without_Delegation
        LIST[ul with 1000 li items]
        LIST -->|1000 event listeners| SLOW[Memory heavy<br/>slow on large lists]
    end
    subgraph With_Delegation
        UL[ul - one listener]
        LI1[li item 1]
        LI2[li item 2]
        LI3[li item 3 - clicked]
        LI1 & LI2 & LI3 -->|event bubbles up| UL
        UL -->|check e.target| HANDLE[Handle click<br/>for any li]
    end
    subgraph Dynamic_Items
        ADD[Add new li later] --> WORKS[Already handled<br/>no new listener needed]
    end
style SLOW fill:#f85149,color:#fff
style UL fill:#238636,color:#fff
style WORKS fill:#238636,color:#fff

Common Misconception

Event delegation is a premature optimisation — for dynamic lists (virtual infinite scroll, real-time feeds), delegation is not optional; attaching listeners to each item causes memory leaks.

Why It Matters

Attaching click listeners to 1000 list items creates 1000 event listener objects in memory; delegation uses one — critical for long lists and dynamically rendered content.

Common Mistakes

  • Not checking event.target matches the expected element — clicks on child elements inside the item bubble up and trigger the handler with the wrong target.
  • Using event.target instead of event.target.closest() — clicks on child elements need .closest() to find the intended parent.
  • Delegating to document or body for all events — too broad; delegate to the nearest stable ancestor.
  • Not stopping propagation when needed — events bubble past the delegation container if not handled.

Code Examples

✗ Vulnerable
// 1000 listeners — memory intensive, breaks for dynamic items:
document.querySelectorAll('.item').forEach(item => {
    item.addEventListener('click', handleClick);
    // Added items have no listener
    // Removed items hold listener in memory (leak)
});
✓ Fixed
// Single delegated listener — works for dynamic items:
document.querySelector('.list').addEventListener('click', (e) => {
    // Find the closest .item ancestor of the clicked element:
    const item = e.target.closest('.item');
    if (!item) return; // Click was outside an item
    handleClick(item);
});

Added 15 Mar 2026
Edited 22 Mar 2026
Views 54
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 2 pings S 0 pings S 0 pings M 0 pings T 0 pings W 1 ping T 0 pings F 0 pings S 1 ping S 1 ping M 2 pings T 0 pings W 2 pings T 1 ping F 2 pings S 3 pings S 1 ping M 1 ping T 1 ping W 1 ping T
Google 1
Scrapy 1
Scrapy 11 Amazonbot 10 Google 5 Ahrefs 4 ChatGPT 4 Perplexity 4 SEMrush 4 Unknown AI 3 Majestic 1 Meta AI 1
crawler 44 crawler_json 2 pre-tracking 1
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: Low
⚡ Quick Fix
Attach one listener to a common parent instead of one per child — event.target identifies which child was clicked, and dynamically added children are handled automatically
📦 Applies To
javascript ES5 web
🔗 Prerequisites
🔍 Detection Hints
Adding event listener to every list item in a loop; listeners not working on dynamically added elements; memory leak from many individual listeners
Auto-detectable: ✓ Yes eslint chrome-devtools
⚠ Related Problems
🤖 AI Agent
Confidence: Low False Positives: High ✗ Manual fix Fix: Medium Context: Function

✓ schema.org compliant