Advanced Event Handling
addEventListener has options most developers never use. once removes itself. passive unlocks scroll performance. Custom events let any part of the app broadcast to any other.
addEventListener has a third argument that most tutorials skip. It contains options that change when and how the handler fires. Understanding these unlocks better performance and cleaner communication patterns in vanilla JS apps.
The Essentials
once: true: The handler fires exactly once, then removes itself automatically. No manual cleanup needed.passive: true: Tells the browser your handler will not callpreventDefault()in this event. Enables the browser to optimize scroll rendering without waiting for your code to finish.removeEventListener: Manually removes a specific handler from an element. Used in SPAs when navigating away from a route that registered handlers on elements that persist in the DOM.- Custom events: You can create and dispatch your own events on any DOM element or on
window. Any other part of the app can listen for them. This is broadcast-style communication without tight coupling. - Multiple handlers fire synchronously: If ten functions are registered for the same event on the same element, they fire one after another in the order they were registered.
The Third Argument: Options
element.addEventListener('click', handler, { once: true, passive: false });once
Setting once: true makes the handler a one-shot listener. After it fires the first time, it is automatically removed:
modal.addEventListener('click', closeModal, { once: true });
// closeModal fires the first time the modal is clicked, then is goneThis is useful for setup operations that should happen exactly once: first-time user onboarding, a one-time permission prompt, or any event you only need to handle once per page session.
passive
This one has a subtle relationship with scroll performance.
By default, the browser cannot start scrolling until your scroll or touchmove event handler finishes. This is because you might call event.preventDefault() in that handler, which would cancel the scroll. The browser waits to find out.
On a heavy page, a slow handler in a scroll listener makes the page feel choppy: your finger moves but the content does not follow immediately.
Setting passive: true is a promise to the browser: "I will not call preventDefault() in this handler. You can start scrolling immediately without waiting for my code."
document.addEventListener('scroll', updateProgressBar, { passive: true });For scroll and touch handlers that are purely observational (tracking scroll position, updating a progress indicator), passive: true is the right choice.
Removing Event Listeners
Handlers registered with addEventListener stay registered until you explicitly remove them or the element is garbage collected. In a single-page application where elements persist across route changes, stale handlers can accumulate.
function handleClick() { /* ... */ }
button.addEventListener('click', handleClick);
// Later, when navigating away:
button.removeEventListener('click', handleClick);removeEventListener requires the exact same function reference that was used in addEventListener. An inline function declared inside addEventListener cannot be removed because you have no reference to it. For handlers you expect to clean up, store the function in a variable first.
Custom Events
The DOM event system is not limited to built-in events. You can define your own and dispatch them on any element or on window.
// Dispatch a custom event from one part of the app
const cartEvent = new CustomEvent('cart:updated', {
detail: { itemCount: 3 }
});
window.dispatchEvent(cartEvent);
// Listen from a completely different part of the app
window.addEventListener('cart:updated', (event) => {
cartBadge.textContent = event.detail.itemCount;
});CustomEvent takes an event name (any string) and an options object. The detail property is where you attach arbitrary data to the event. Listeners read it from event.detail.
This is a broadcast pattern: the dispatcher does not know who is listening, and the listeners do not know where the event came from. It is structurally similar to context in React or a pub-sub event bus. The difference is that it uses the DOM API directly rather than a framework abstraction.
How Multiple Handlers Fire
JavaScript is single-threaded. When ten handlers are registered for the same event on the same element, they fire one after another in registration order. There is no parallelism. Handler 2 does not start until Handler 1 returns.
This is a useful guarantee: if order matters, the registration order controls execution order. If you need concurrent behavior, you would need to use Web Workers, which have their own separate thread.
Further Reading and Watching
- MDN: addEventListener - options parameter - Full reference for
once,passive,capture, and other options. - MDN: CustomEvent - Reference for creating and dispatching custom events including the
detailproperty.
Video:
- JavaScript DOM Crash Course - Part 3 by Traversy Media. Covers event handling fundamentals including event delegation and custom events.