What is event delegation and why should I use it?
Attach a single listener to a container and use event.target.closest() to identify what was clicked. It uses less memory and automatically covers elements added after the listener was attached.
document.querySelector('.list').addEventListener('click', (event) => {
const button = event.target.closest('[data-delete]');
if (!button) return;
const id = button.dataset.delete;
removeItem(id);
});One listener handles every button in the list — including ones added ten minutes from now.
#The alternative does not scale
document.querySelectorAll('[data-delete]').forEach((button) => {
button.addEventListener('click', handler);
});A thousand rows means a thousand listeners. Worse, a row added after this runs has no listener at all, which is the source of "it works until I add a new item".
#Why closest() and not target directly
If the button contains an icon, event.target may be the <svg>, not the <button>. closest() walks up from the target until it finds a match, so it works regardless of what was actually clicked.
The if (!button) return guard is essential — clicks elsewhere in the container will also fire the listener.
#It relies on bubbling
Events travel up the tree from the target. A few do not bubble — focus, blur, and most media events. Use focusin/focusout instead, which do.
You can also capture, which delegates non-bubbling events:
form.addEventListener('blur', handler, true); // capture phase#Keep the handler cheap
It runs on every click inside the container. Do the closest() check and bail out first, before any expensive work.