Removal from the DOM is not cleanup
Removing a component’s element does not automatically cancel work that the component started. A window event listener may still respond to clicks or resizing. An interval may keep running, and an observer may continue watching its targets. These callbacks can retain references to component state or attempt to update an element that is no longer on the page.
Treat cleanup as part of the component’s lifecycle: mounting starts work, and disposing stops it. A useful rule is that the code which creates a subscription or schedules repeated work should also define how to end it. This makes ownership visible when the component is reused, replaced, or removed.
Keep a cleanup function beside each setup step
Consider a small status component in a modern browser. It displays the window width, watches its own element for size changes, and refreshes a clock once a second. The environment is ordinary browser JavaScript with a document, ResizeObserver, and AbortController; no framework is required. The component needs three cleanup actions: abort the resize event listener, disconnect the observer, and clear the interval.
One practical design is a mount function that returns a dispose function. During setup, create a controller, add the window resize listener with its signal, create and observe with a ResizeObserver, and store the ID returned by setInterval. The returned dispose function calls controller.abort(), observer.disconnect(), and clearInterval(intervalId). Calling dispose before removing the element makes the intended order easy to see. If another part of the application removes the element, that part must still call dispose; DOM removal alone does not invoke it.
Keep each callback narrow. For example, the window resize handler can update width text, while the observer handles changes to the element’s own dimensions. If both would perform the same work, decide whether one is sufficient instead of maintaining two sources of updates. Cleanup is simpler when setup contains only the work the component actually needs.
Match the cleanup method to the resource
AbortController is convenient for grouping event listeners owned by one component. Pass the same controller’s signal in the options for each listener, then abort it during disposal. If you use removeEventListener instead, retain the original callback and use the same event type, target, and capture setting used when adding it. Creating a new function with identical contents will not remove the original listener.
Observers have their own lifecycle methods. Call disconnect() on a ResizeObserver or MutationObserver when the component no longer needs notifications. If an observer is deliberately shared across components, unobserve a departing component’s element rather than disconnecting everyone. The distinction is ownership: a component should not shut down a shared resource that other components still use.
Use clearInterval for an interval and clearTimeout for a pending timeout. If a callback schedules its next timeout, store the latest ID so disposal can cancel it. For a requestAnimationFrame loop, retain the scheduled frame ID and call cancelAnimationFrame. These cancellation methods stop future scheduled callbacks; they cannot undo a callback already running. If a callback starts asynchronous work that cannot be canceled, check whether the component is still active before applying its result.
Make disposal safe to call twice
A dispose function may be called from more than one path: a navigation action, a parent component’s cleanup, or a test teardown. Make it idempotent by tracking a disposed flag. On the first call, mark the component disposed and release its resources; on later calls, return without repeating the work. The flag also gives delayed callbacks a way to avoid updating removed content.
You can check the pattern manually in a browser: mount the component, trigger a resize, then dispose of it and remove its element. Resize the window again and wait longer than one interval period. No component callback should update state or log output after disposal. Repeat the mount-and-dispose cycle to catch work that accumulates across instances. In an automated test, use controlled timers where available and assert that callbacks stop after disposal.
Finally, distinguish component-owned work from application-wide work. A global keyboard shortcut managed by the application should not be canceled merely because one panel closes. Conversely, a listener added to window solely for that panel belongs in the panel’s cleanup. Clear ownership, rather than a particular library or syntax, is what makes component removal predictable.



