JAVASCRIPT / THE BROWSER: DOM, EVENTS, AND STORAGE
Listening for events with addEventListener
Attach, stack, and remove DOM event handlers with addEventListener, using once, signal, and currentTarget to control how your callback runs.
What you will learn
- Register several handlers for one event type on the same element, in order
- Remove a listener by passing the identical function reference and capture flag
- Use { once: true } or an AbortController signal instead of manual removeEventListener
- Read event.currentTarget instead of this inside arrow-function handlers
Understanding Listening for events with addEventListener
Every DOM node carries an internal list of registered listeners, and addEventListener appends to that list instead of writing to a single slot. That is why three unrelated functions can all respond to the same click on one button, and why they are invoked in the order they were registered. You hand over the function itself, not a call to it: the browser stores the reference and invokes it later, passing an event object as the first argument.
A listener's identity is the triple of event type, callback, and capture flag. Two things follow from that. Registering the exact same triple a second time is silently discarded, so a top-level named handler re-attached on every re-render still fires only once. And removeEventListener can only find a listener when all three parts match, which makes an inline arrow function effectively permanent, because you never kept a reference to it.
When the browser calls your callback it sets this to the element you attached the listener to, which is the same value as event.currentTarget. An arrow function ignores that and keeps this from its surrounding scope, so with arrows you must read event.currentTarget to reach the element. The older property style, el.onclick = fn, is a single slot: assigning twice overwrites the first handler, while addEventListener has room for both.
const button = document.createElement('button');
button.textContent = 'Save';
function logClick(event) {
console.log('handler 1 saw', event.type, 'on', event.currentTarget.textContent);
}
button.addEventListener('click', logClick);
button.addEventListener('click', () => console.log('handler 2 ran'));
button.addEventListener('click', logClick); // same type, same function: ignored
button.click();
button.removeEventListener('click', logClick);
console.log('--- logClick removed ---');
button.click();addEventListener appends a callback to an element's listener list, and that listener is identified by its type, callback reference, and capture flag.
Worked examples
once and an abort signal
Two ways to let the browser unregister a listener for you instead of calling removeEventListener.
const box = document.createElement('div');
const controller = new AbortController();
box.addEventListener('ping', () => console.log('once handler'), { once: true });
box.addEventListener('ping', () => console.log('signal handler'), { signal: controller.signal });
box.dispatchEvent(new Event('ping'));
box.dispatchEvent(new Event('ping'));
controller.abort();
box.dispatchEvent(new Event('ping'));
console.log('done');Example explained
Line 1{ once: true } removes the listener right after its first invocation, so it never sees the second dispatch.
Line 2{ signal } ties the listener's lifetime to an AbortController, which is useful when many listeners must be torn down together.
Line 3controller.abort() unregisters every listener created with that signal, so the third dispatch logs nothing.
Line 4dispatchEvent(new Event('ping')) fires a custom type synchronously, which is handy for testing without user input.
this versus event.currentTarget
Shows why an arrow handler cannot use this to reach the element it is attached to.
const input = document.createElement('input');
input.value = 'hello';
input.addEventListener('check', function (event) {
console.log('function form: this.value =', this.value);
console.log('this === event.currentTarget ->', this === event.currentTarget);
});
input.addEventListener('check', (event) => {
console.log('arrow form: currentTarget.value =', event.currentTarget.value);
});
input.dispatchEvent(new Event('check'));Example explained
Line 1In a regular function the browser binds this to the element the listener sits on.
Line 2The comparison proves this and event.currentTarget refer to the same node during dispatch.
Line 3The arrow function has no own this, so event.currentTarget is the reliable way to reach the input.
onclick overwrites, addEventListener stacks
Compares the single-slot handler property with the listener list.
const a = document.createElement('button');
a.onclick = () => console.log('onclick A');
a.onclick = () => console.log('onclick B');
a.addEventListener('click', () => console.log('listener C'));
a.addEventListener('click', () => console.log('listener D'));
a.click();
a.onclick = null;
console.log('--- onclick cleared ---');
a.click();Example explained
Line 1The second assignment to a.onclick replaces the first, so 'onclick A' is gone forever.
Line 2The onclick slot was claimed before C and D were added, so it still runs first in registration order.
Line 3Setting a.onclick = null clears that one slot and leaves the two addEventListener callbacks untouched.
Important notes
event.currentTarget is only set while the event is being dispatched. If you stash the event and read it inside a setTimeout it will be null, so capture the element in a variable first.
The capture flag is part of a listener's identity but once and passive are not: removeEventListener('scroll', fn) removes a listener added with { passive: true }, but not one added with { capture: true }.
Common mistakes
Writing addEventListener('click', handleClick()) with parentheses: the function runs immediately at registration time and its return value (usually undefined) is registered, so clicking does nothing.
Adding an inline arrow such as () => save() and later calling removeEventListener('click', () => save()): the second arrow is a different object, nothing is removed, and if the setup code re-runs the save fires once per registration.
Passing the type as 'onclick' or 'Click' instead of 'click': no error is thrown because any string is a valid custom type, and the listener simply never fires.
Try it yourself
Change, predict, then run
On a blank page, create a button with document.createElement, attach a named click handler that logs an incrementing counter plus a second { once: true } handler that logs 'first click only', append the button to document.body and click it three times. Then call removeEventListener with the named function and confirm further clicks log nothing.
Open the JavaScript workspaceCheck your understanding
A setup function runs each time new data loads and always calls list.addEventListener('click', handleRowClick) with the same top-level named function. After the data has loaded five times, how many lines does one click produce?
- Five, because addEventListener appends a new listener on every call
- Once, because an element can hold only one listener per event type
- Once, because re-registering the same type, callback, and capture flag is ignored
- None, because the later registrations replace the first one
Show answer
The listener list is keyed by type, callback reference, and capture flag, so the four repeat registrations of the identical named function are discarded and the handler runs once. Five would be correct only if each call passed a freshly created function, such as an inline arrow, since those are distinct objects. The claim that an element holds one listener per type is wrong: different functions for the same type all stack and all run.