Step 07

Destruction

Half of putting a page in a new state is taking the old state away. Removal looks like the easy third of the add–edit–delete trio, and the API is genuinely small — but it hides the two classic traps of DOM programming: deleting from a collection while you iterate it, and removing a node while something else still holds a reference to it. This step covers the small API and both traps.

Learning Objectives

  • Remove elements with remove() and read the older removeChild() pattern in existing code
  • Swap nodes with replaceWith() and empty containers efficiently
  • Explain why deletion loops over live collections run bottom-up
  • Clean up event listeners — by delegation, and by AbortController

Taking a node out

The modern call is the obvious one: the element removes itself.

el.remove();

The original DOM had no such method. Removal was a parental act: you found the parent and asked it to disown the child, and the call handed the removed node back in case you wanted to keep it.

const removed = parent.removeChild(child); // returns the removed node parent.replaceChild(newChild, oldChild); // likewise parental

You will read removeChild in nearly every codebase older than a few years, so know it — but write remove() and its sibling replaceWith(), which put the verb on the node you are actually acting on. A removed node is not destroyed, by the way: if you kept a reference, it is merely detached, and you can append it somewhere else. Moving a node is removing and re-inserting it.

Emptying a container

To clear a list wholesale, the blunt instrument is fine, and it is what the demo's Clear button does:

list.innerHTML = '';

If you loop instead — say, removing only some children — remember what step 2 taught about live collections: children re-indexes itself as you delete, so a forward loop skips every other element. The classic fix is to run the loop from the end, where deletions cannot shift what you have not visited yet:

const items = list.children; // live! for (let i = items.length - 1; i >= 0; i--) { items[i].remove(); // deleting never shifts what remains }
The skip bug. Delete index 0 of a live collection and the element at index 1 becomes index 0 — then your loop moves on to index 1 and never looks at it. Iterate a static querySelectorAll list, or go bottom-up. This bug is a rite of passage; passage is faster if you know it is coming.

Live demo

Add items, remove them one at a time, clear the lot. Note that the per-item Remove buttons carry no listeners of their own — one delegated listener on the container (step 4 of the Events series) handles every button that will ever exist in it:

list.addEventListener('click', (e) => { const btn = e.target.closest('button[data-action="remove"]'); if (!btn) return; btn.closest('.item')?.remove(); });

Destruction is more than nodes

Removing an element does not remove what points at it. A listener registered on window that closes over your element, an interval that touches it, a Map that keys on it — any of these keeps the “removed” node alive and the work firing. This is the leak everyone writes at least once. Two habits prevent it. Delegation is the first: listeners live on the container, so removing children removes nothing that needs cleanup. The second is AbortController, which the demo's second section uses — pass a signal when you listen, and one abort() detaches everything registered with it:

controller = new AbortController(); const { signal } = controller; window.addEventListener('resize', onResize, { signal }); // later, one call cleans up every listener that shares the signal controller.abort();
Group your teardown. One controller per component or view, passed to every addEventListener it makes, turns cleanup from a checklist into a single call — the same pattern that cancels a fetch.

Next Steps

You can now select, walk, read, write, create, and destroy — the full manipulation vocabulary. In Step 8: Geometry and observers, we ask the tree where things actually are:

  • Measure with getBoundingClientRect() and the offset/client/scroll families
  • Understand what reading layout costs
  • Let IntersectionObserver answer “is it visible?” without polling