Home/Learn/JavaScript & TypeScript/Events — Bubbling, Delegation and Cleanup

Events — Bubbling, Delegation and Cleanup

Intermediate
The Browser

Events travel down to the target and back up again. Bubbling is what makes delegation possible, and delegation is what makes one listener handle a thousand rows.

Overview

An event fires on an element and then travels: first down from the document to the target (capture), then back up (bubble). Almost all handlers listen during bubbling, which means a listener on a container sees events from every descendant. That is event delegation, and it is why a table with a thousand rows needs one click listener rather than a thousand. Understanding the phases also explains stopPropagation, why preventDefault does not stop bubbling, and why removing a listener requires the same function reference you added.

The Two Phases

Capture goes down, bubble comes back up. The third argument to addEventListener chooses which phase you listen in.

Capture, target, bubble
// document -> ... -> parent -> TARGET -> parent -> ... -> document
//         capture phase            |         bubble phase

parent.addEventListener('click', h)              // bubble (default)
parent.addEventListener('click', h, true)        // capture
parent.addEventListener('click', h, { capture: true, once: true })

// e.target  — where it actually happened
// e.currentTarget — where this listener is attached

parent.addEventListener('click', e => {
  e.target        // the button that was clicked
  e.currentTarget // parent
})

e.stopPropagation()   // stop travelling further
e.preventDefault()    // cancel the default action; does NOT stop bubbling

Delegation

One listener on a container, using closest() to find which item was hit. Works for elements added later, which direct listeners do not.

One listener, any number of rows
// One listener for any number of rows, present or future
list.addEventListener('click', (e) => {
  const row = e.target.closest('[data-problem-id]')
  if (!row) return                      // clicked the gap, not a row
  openProblem(row.dataset.problemId)
})

// Without delegation you would attach per row, and re-attach
// every time the list re-renders.

// Some events do not bubble — use capture or the focus* variants:
//   focus / blur      -> do not bubble; focusin / focusout do
//   mouseenter/leave  -> do not bubble; mouseover/mouseout do

Removing Listeners

removeEventListener needs the identical function reference. An inline arrow can never be removed, which is a steady source of leaks.

The same reference, or an AbortController
// Cannot be removed — a new function every time
el.addEventListener('click', () => handle())
el.removeEventListener('click', () => handle())   // different function

// Correct — keep the reference
const onClick = () => handle()
el.addEventListener('click', onClick)
el.removeEventListener('click', onClick)

// Easier — AbortController removes many at once
const c = new AbortController()
el.addEventListener('click', onClick, { signal: c.signal })
window.addEventListener('resize', onResize, { signal: c.signal })
c.abort()          // removes both

// In React, that maps onto effect cleanup:
useEffect(() => {
  const c = new AbortController()
  window.addEventListener('keydown', onKey, { signal: c.signal })
  return () => c.abort()
}, [])

Key Points to Remember

  • 1Events capture down to the target then bubble back up; listeners default to the bubble phase
  • 2e.target is where the event happened, e.currentTarget is where the listener is attached
  • 3Delegation puts one listener on a container and uses closest() — it also works for elements added later
  • 4preventDefault cancels the default action; stopPropagation stops travel — they are unrelated
  • 5removeEventListener needs the identical function reference; an AbortController signal removes many at once

Interview Questions

Sign in to ask Aria
1

What is event delegation and why is it useful?

Medium
2

What is the difference between e.target and e.currentTarget?

Medium
3

Why does removeEventListener with an inline arrow function fail to remove the listener?

Medium

Ask Aria about Events — Bubbling, Delegation and Cleanup

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…