RoadmapDay 9 / 80
JavaScript CoreMonth 1 · Week 2

Day 9: Event Bubbling, Capturing, and Event Delegation Patterns

Master the DOM event flow well enough to build one delegated listener that replaces hundreds of individual ones, and to reason about stopPropagation correctly.

Mark this day complete

Study

Concepts

Three phases of every DOM event

When an event fires on a DOM node, it travels in three phases: Capturing (from `window` down to the target, only observed by listeners registered with `{ capture: true }`), Target (the element the event actually happened on), and Bubbling (back up from the target to `window`, the default phase most listeners observe).

`event.stopPropagation()` halts further travel through remaining phases; `event.stopImmediatePropagation()` additionally prevents other listeners on the SAME element/phase from firing. `event.preventDefault()` is unrelated to propagation — it only cancels the browser's default action (e.g. navigating a link, submitting a form).

Event delegation: one listener, many children

Because events bubble, you can attach a single listener to a stable parent and inspect `event.target` (the actual element clicked) versus `event.currentTarget` (the element the listener is attached to) to figure out which child was interacted with. This avoids attaching/removing hundreds of listeners for dynamically added/removed list items — a major performance and memory win.

React's synthetic event system historically attached one listener at the document root (React 17+ attaches at the root container) and dispatched synthetic events down through its own tree — the same delegation idea applied at the framework level, which is why removing a DOM node manually outside React can sometimes leave React's internal event map out of sync.

See It

Visualizations

Visualization

Capturing → Target → Bubbling for a click on <li>

window → document → <ul> (capture phase)

only capture:true listeners fire, top-down

<li> (target phase)

the element actually clicked

<li> → <ul> → document → window (bubble phase)

default listeners fire, bottom-up

Build It

Code Examples

Event delegation for a dynamic list (one listener total)

js
const list = document.querySelector('#todo-list');

// ONE listener handles clicks for every <li>, present or future
list.addEventListener('click', (event) => {
  const item = event.target.closest('li[data-id]');
  if (!item || !list.contains(item)) return; // clicked outside an <li>

  if (event.target.matches('.delete-btn')) {
    item.remove(); // no need to detach a per-item listener first
  } else {
    item.classList.toggle('done');
  }
});

// Adding 1,000 new <li> items needs ZERO new listeners
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.dataset.id = String(i);
  li.innerHTML = `Task ${i} <button class="delete-btn">x</button>`;
  list.appendChild(li);
}

target vs currentTarget, and stopping propagation correctly

js
outer.addEventListener('click', (e) => {
  console.log('target:', e.target.tagName);        // the actual clicked element
  console.log('currentTarget:', e.currentTarget.tagName); // always OUTER here
});

inner.addEventListener('click', (e) => {
  e.stopPropagation(); // prevents the outer listener above from firing
  console.log('inner handled it, bubbling stopped');
});

Remember

Key Takeaways

  • Events flow capture (down) → target → bubble (up) — most code only ever listens on the bubble phase.
  • target = where the event happened; currentTarget = the element the listener is attached to.
  • stopPropagation halts travel; preventDefault cancels the browser default action — they are independent.
  • Delegation = one listener on a stable ancestor + event.target.closest() to identify the real target.
  • Delegation scales to dynamically added/removed children with zero extra listener management.

Do It

Practice

  1. 1Rebuild a todo list UI using a single delegated click listener instead of one listener per item.
  2. 2Write a nested 3-div example and log the phase order for both capture and bubble listeners to confirm the flow.
  3. 3Explain a real bug scenario where forgetting stopPropagation causes a modal-close handler to also trigger a parent click handler.