The DOM and events
Capturing, bubbling, delegation, and safe dynamic lists.
Events travel down to their target and bubble back up — and one parent listener using that bubble beats a hundred individual ones. Interviewers ask this because delegation, propagation control and dynamic rows are daily DOM work.
Real-life situation
A to-do list adds rows all day. Binding each row's button misses every
future row, so deletes silently stop working after the first add. One
listener on the list, reading event.target.closest("li"), handles present
and future rows alike — try it live below, including the add button.
Seen in: jQuery's .on() unified direct and delegated binding this way
(jQuery 1.7, 2011, on() docs).
Visual
Down and back upbrowser
Capture path
Target
Bubble path
empty
Console
—
Click on Buy. The click is born at the button — but listeners on ancestors get their turn too, in two passes.
<main id="page">
<section id="card">
<button id="buy">Buy</button>
</section>
</main>Try it
const list = document.querySelector("#todo");
const status = document.querySelector("#status");
let added = 0;
list.addEventListener("click", function (event) {
const item = event.target.closest("li");
if (!item) {
return;
}
item.remove();
status.textContent = "left: " + list.children.length;
});
document.querySelector("#add").addEventListener("click", function () {
added = added + 1;
const item = document.createElement("li");
item.textContent = "task " + added;
const remove = document.createElement("button");
remove.textContent = "done";
item.appendChild(remove);
list.appendChild(item);
});
console.log("ready:" + list.children.length);Isolated browser frame. Network and imports are disabled.
Click a done button, then add a task and delete it too — no new listeners
were ever bound. Then try removing the closest line and click the list
padding to see why the guard matters.
When to delegate, when to bind directly
Delegate dynamic lists and repeated controls from a stable parent. Bind directly for one-off controls with no future siblings. Stop propagation only when ancestors must not react; prevent default to cancel the browser action.
No DOM here — guess first, then reveal.
console.log(typeof document);Interview questions
What is the difference between capturing and bubbling?basic
An event travels TWICE: down from window to the target (capturing phase), then back up to window (bubbling phase). Listeners default to bubble-phase; pass { capture: true } to catch the way down. Almost all everyday code uses bubbling (delegation depends on it); capturing is for intercepting events before descendants see them, like global shortcuts or modal traps.
Interviewers also ask
- How do you listen on the way down?
What is event delegation and why is it better than many listeners?basic
One listener on a parent, using event.target to find which child was actually hit — instead of a listener per child. It is faster to set up, uses constant memory, and automatically covers children added LATER (the killer feature for dynamic lists). The classic demo: one <ul> listener handling delete buttons for rows that don't exist yet. Pair it with closest() to find the relevant ancestor.
Interviewers also ask
- How do later-added rows work with no new listeners?
What is the difference between stopPropagation and preventDefault?intermediate
stopPropagation halts the event's JOURNEY (no more listeners up or down the tree); preventDefault cancels the browser's DEFAULT ACTION (follow the link, submit the form) while the event keeps traveling. They compose: a validated form calls preventDefault always, stopPropagation only when ancestors must not react. Mixing them up either breaks delegation or lets the page navigate away mid-handler.
Interviewers also ask
- When would you call both?
What is event.target versus event.currentTarget?basic
target is where the event STARTED (the clicked button); currentTarget is whose listener is RUNNING right now (the <ul> you delegated to). They coincide only without delegation. The standard delegation line reads e.target.closest('li') inside a currentTarget handler — target finds the item, currentTarget anchors the list. Confusing them breaks every delegated handler.
Interviewers also ask
- Which one do you use inside a delegated handler?
Why do dynamically added elements miss their handlers, and what is the fix?intermediate
Because listeners attach to elements that exist at bind time — rows added later were never bound. Fixes, best first: delegate from a stable parent (future rows included automatically); re-bind after each render (fragile, leaks duplicates); or bind at creation inside the row factory. If an interview demo "works until I add a row", delegation is the expected diagnosis and the expected fix.
Interviewers also ask
- What goes wrong with re-binding after render?
How do you safely add user content to the page?basic
With textContent (or createElement + textContent), never innerHTML with user strings — innerHTML parses markup, so a username containing script markup or an onerror image runs attacker code (XSS). textContent treats everything as inert text. The rule is absolute for beginners: innerHTML only with developer-written, static markup; everything from users goes through textContent.
Interviewers also ask
- What attack does innerHTML enable?
What is the difference between input, change and click for form fields?intermediate
input fires on EVERY keystroke (live search, validation-as-you-type); change fires on COMMIT (blur for text, selection for dropdowns and checkboxes — the "final answer" event); click is for buttons and toggles. Debounce belongs on input, saves belong on change/submit. Using click on a text field or change for live feedback are the two classic mismatches.
Interviewers also ask
- Which event drives a live search box?
How do timers and events interact when the thread is busy?intermediate
They wait. Handlers and timer callbacks are tasks like any other: while your code runs, clicks queue silently and timers go overdue, then everything fires in order once the stack clears. Users experience this as frozen buttons and late updates during heavy work — the same single-thread story as blocking, wearing an events costume. Keep handlers short or yield.
Interviewers also ask
- Where do clicks go during a long task?
What does { once: true } do on addEventListener?basic
The listener removes ITSELF after its first run — perfect for one-shot onboarding tips, first-click analytics, and any "do this exactly once" wiring. It also prevents the classic leak of forgotten listeners on temporary UI. For repeated handling with automatic cleanup on teardown, pair a normal listener with AbortController's signal instead.
Interviewers also ask
- How do you remove many listeners at once?
How does the AbortController signal option replace removeEventListener?intermediate
Pass { signal } at bind time; controller.abort() removes every listener sharing that signal in one call — no function-reference bookkeeping, no missed removals. It is the modern teardown for components: one controller per mount, abort on unmount. Bonus: the same signal can cancel the component's fetches, unifying DOM and network cleanup.
Interviewers also ask
- What else can one signal cancel?
Key takeaways
- Events capture down, then bubble up; listeners default to bubbling.
- Delegate from stable parents; read
event.target, anchorcurrentTarget. textContentfor user strings;innerHTMLonly for static markup.
References
Last verified: 2026-10-12.