Events properly – delegation, custom events, passive listeners, and cleanup. Beginner tutorials attach handlers to single buttons; real UIs need patterns that scale and do not leak memory.
Event delegation
const list = document.querySelector('#todo-list');
list.addEventListener('click', (event) => {
const button = event.target.closest('[data-action]');
if (!button || !list.contains(button)) return;
const { action } = button.dataset;
const item = button.closest('[data-id]');
if (action === 'delete') removeItem(item.dataset.id);
if (action === 'toggle') toggleItem(item.dataset.id);
});
closest walks up from the click target. One listener handles current and future rows.
Custom events
const cart = document.querySelector('#cart');
cart.addEventListener('cart:updated', (event) => {
renderTotal(event.detail.total);
});
function emitCartUpdated(total) {
cart.dispatchEvent(new CustomEvent('cart:updated', {
bubbles: true,
detail: { total },
}));
}
Namespaced event names (cart:updated) reduce collisions with native events. Use detail for payload data.
Passive listeners for scroll/touch
window.addEventListener('scroll', onScroll, { passive: true });
Tells the browser you will not call preventDefault, so scrolling can proceed smoothly. Required for touch performance on mobile in many cases.
Cleanup with AbortController
const controller = new AbortController();
const { signal } = controller;
button.addEventListener('click', onSave, { signal });
fetch('/api/config', { signal }).then(render);
function teardown() {
controller.abort(); // removes listeners using signal
}
One controller can unify DOM listeners and in-flight fetch for a component or modal lifecycle.
once and capture
{ once: true } removes the listener after the first fire – handy for intro tours or waiting for a transition. Capture phase ({ capture: true }) runs on the way down; rarely needed except for focus traps or intercepting before children.
Next: the constraint validation API for forms that behave well without reinventing every rule in JavaScript.

