disconnectedCallback
This lesson explains disconnectedCallback, when the browser calls it, and how to clean up timers, listeners, observers, and network requests when a custom element leaves the DOM.
What Is disconnectedCallback?
disconnectedCallback() is a custom element lifecycle
method that runs when the element is removed from the document.
Use it to tear down anything created in
connectedCallback() so your component does not leak
memory or keep running in the background.
In single-page applications, components mount and unmount
frequently. Reliable cleanup in
disconnectedCallback() keeps long-running apps stable.
| Concept | Description |
disconnectedCallback() | Runs when the element leaves the DOM |
| Lifecycle Type | Custom element lifecycle callback |
| Common Use | Cleanup timers, listeners, observers, requests |
| Runs Multiple Times? | Yes, each time the element is removed |
| Paired With | connectedCallback() for setup |
| Main Benefit | Prevents memory leaks and stale side effects |
Basic disconnectedCallback Example
class LiveClock extends HTMLElement {
connectedCallback() {
this.render();
this._timer = setInterval(() => this.render(), 1000);
}
disconnectedCallback() {
clearInterval(this._timer);
this._timer = null;
}
render() {
this.textContent = new Date().toLocaleTimeString();
}
}
customElements.define("live-clock", LiveClock);
The interval starts on connect and stops on disconnect. Without
clearInterval(), the timer would keep firing after
the element was removed.
When disconnectedCallback Runs
| Scenario | Callback Runs? |
element.remove() | Yes |
Parent replaces children with innerHTML | Yes |
| SPA route change unmounts the component | Yes |
| Element moved within the same document | Sometimes disconnect then reconnect |
| Attribute change only | No |
| Element hidden with CSS | No |
Disconnect means the element is no longer in the document tree, not
merely hidden or inactive visually.
Pairing with connectedCallback
class DataPanel extends HTMLElement {
connectedCallback() {
if (this._initialized) return;
this._initialized = true;
this._controller = new AbortController();
window.addEventListener(
"resize",
() => this.updateLayout(),
{ signal: this._controller.signal }
);
this.fetchData();
}
disconnectedCallback() {
this._controller?.abort();
this._controller = null;
this._initialized = false;
}
}
Every resource created in connectedCallback() should
have a matching cleanup step in
disconnectedCallback(). Reset flags such as
_initialized if the element may connect again later.
What to Clean Up
setInterval() and setTimeout() requestAnimationFrame() loops - Window, document, and media event listeners
IntersectionObserver, MutationObserver, ResizeObserver AbortController for fetch and signal-based listeners - WebSocket, EventSource, and subscription connections
- Third-party library instances tied to the element
Disconnecting Observers
connectedCallback() {
this._observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) this.loadContent();
});
this._observer.observe(this);
}
disconnectedCallback() {
this._observer?.disconnect();
this._observer = null;
}
Observers keep references to elements and callbacks. Always call
disconnect() when the component unmounts.
AbortController Cleanup Pattern
async fetchData() {
this._abortController = new AbortController();
try {
const response = await fetch(this.dataset.url, {
signal: this._abortController.signal
});
const data = await response.json();
this.render(data);
} catch (error) {
if (error.name !== "AbortError") throw error;
}
}
disconnectedCallback() {
this._abortController?.abort();
this._abortController = null;
}
Aborting in-flight fetch requests prevents state updates on
components that are no longer in the DOM.
Removing Event Listeners
connectedCallback() {
this._onKeyDown = (event) => {
if (event.key === "Escape") this.close();
};
document.addEventListener("keydown", this._onKeyDown);
}
disconnectedCallback() {
document.removeEventListener("keydown", this._onKeyDown);
this._onKeyDown = null;
}
Store listener references on this so you can remove
the exact same function in disconnect. Anonymous functions cannot
be removed later.
Reconnect After Disconnect
Custom elements can leave and re-enter the DOM. That means
connectedCallback() and
disconnectedCallback() may run multiple times during
the element's life.
disconnectedCallback() {
this.cleanup();
this._initialized = false;
}
connectedCallback() {
if (this._initialized) return;
this._initialized = true;
this.setup();
}
Design setup and cleanup to be idempotent so reconnecting does not
create duplicate listeners or timers.
disconnectedCallback in Lit
disconnectedCallback() {
super.disconnectedCallback();
this._chart?.destroy();
this._chart = null;
}
Lit components should call super.disconnectedCallback()
so the base class can complete its own teardown before your custom
cleanup runs.
SPA Route Change Example
// Route unmount removes the component from the page
router.onLeave(() => {
viewContainer.innerHTML = "";
// disconnectedCallback runs on child custom elements
});
Framework route changes often destroy DOM subtrees. If your custom
elements register global listeners or polling in connect, they must
clean up on disconnect or the SPA will leak memory over time.
connectedCallback vs disconnectedCallback
| Callback | When | Typical Actions |
connectedCallback() | Element enters the DOM | Render, listen, fetch, start timers |
disconnectedCallback() | Element leaves the DOM | Stop timers, abort fetch, remove listeners |
Full Cleanup Example
class MetricsWidget extends HTMLElement {
connectedCallback() {
if (this._active) return;
this._active = true;
this._abort = new AbortController();
this._pollId = setInterval(() => this.refresh(), 30000);
window.addEventListener(
"online",
() => this.refresh(),
{ signal: this._abort.signal }
);
this.refresh();
}
disconnectedCallback() {
clearInterval(this._pollId);
this._abort?.abort();
this._pollId = null;
this._abort = null;
this._active = false;
}
async refresh() { /* fetch and render */ }
}
When disconnectedCallback Matters Most
- Your component starts timers or polling on connect.
- You attach listeners to
window or document. - You create observers or long-lived subscriptions.
- You fetch data that may finish after unmount.
- The component is used in SPA or micro frontend shells.
- You integrate third-party widgets with their own teardown APIs.
disconnectedCallback Best Practices
- Mirror every connect setup action with a disconnect cleanup.
- Store listener and timer IDs on
this for removal. - Abort in-flight async work when the element unmounts.
- Null out references after cleanup to avoid stale state.
- Reset initialization flags if reconnect is possible.
- Call
super.disconnectedCallback() in Lit subclasses. - Test mount/unmount cycles in development.
Common disconnectedCallback Mistakes
- Starting timers in connect with no cleanup on disconnect.
- Adding document listeners with anonymous functions.
- Assuming disconnect runs only once per element.
- Updating DOM after async work completes post-unmount.
- Forgetting to disconnect observers and subscriptions.
- Leaving
_initialized true after disconnect. - Skipping
super.disconnectedCallback() in Lit components.
Key Takeaways
disconnectedCallback() runs when the element leaves the DOM. - It is the correct place for cleanup and teardown logic.
- Pair it with
connectedCallback() for balanced lifecycle design. - Timers, listeners, observers, and fetch requests must be stopped or removed.
- Elements may disconnect and reconnect multiple times.
- Good cleanup prevents memory leaks in dynamic applications.
Pro Tip
During code review, scan connectedCallback() for every
side effect and verify there is a matching line in
disconnectedCallback(). That simple habit catches most
lifecycle leaks early.