Lifecycle Callbacks
This lesson explains custom element lifecycle callbacks and when the browser calls them during connect, disconnect, attribute changes, and document adoption.
What Are Lifecycle Callbacks?
Lifecycle callbacks are methods on a custom element class that the
browser calls at key moments in the element's life. They let
you run setup, react to changes, and clean up resources at the
right time.
Custom elements have four standard lifecycle callbacks. Understanding
when each one runs is essential for building reliable Web
Components.
| Callback | When It Runs |
connectedCallback() | Element is added to the DOM |
disconnectedCallback() | Element is removed from the DOM |
attributeChangedCallback() | Observed attribute changes |
adoptedCallback() | Element moves to a new document |
Basic Lifecycle Example
class LoggerPanel extends HTMLElement {
connectedCallback() {
console.log("connected");
this.render();
}
disconnectedCallback() {
console.log("disconnected");
this.cleanup();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
static get observedAttributes() {
return ["status"];
}
render() {
this.textContent =
`Status: ${this.getAttribute("status") || "idle"}`;
}
cleanup() {
this._timer && clearInterval(this._timer);
}
}
customElements.define("logger-panel", LoggerPanel);
This component renders on connect, updates when
status changes, and cleans up when removed.
Custom Element Lifecycle Flow
| Stage | Callback | Typical Use |
| Created | constructor() | Initialize fields, attach shadow root |
| Connected | connectedCallback() | Render, fetch data, add listeners |
| Attribute changed | attributeChangedCallback() | Sync UI with attribute updates |
| Disconnected | disconnectedCallback() | Remove listeners, clear timers |
| Adopted | adoptedCallback() | Reinitialize document-specific behavior |
connectedCallback()
connectedCallback() {
if (this._initialized) return;
this._initialized = true;
this.render();
this.addEventListeners();
this.startPolling();
}
connectedCallback() runs when the element enters the
document. Use it for initial rendering, event listeners, and data
fetching. It can run more than once if the element is moved in the
DOM.
disconnectedCallback()
disconnectedCallback() {
clearInterval(this._timer);
this._observer?.disconnect();
this._abortController?.abort();
this._initialized = false;
}
disconnectedCallback() runs when the element leaves the
document. Always clean up timers, observers, listeners, and network
requests here to avoid memory leaks.
attributeChangedCallback()
class StatusBadge extends HTMLElement {
static get observedAttributes() {
return ["variant", "label"];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
if (name === "variant") {
this.className = `badge badge-${newValue}`;
}
if (name === "label") {
this.textContent = newValue;
}
}
}
This callback runs only for attributes listed in
observedAttributes. It keeps the component UI in sync
with declarative HTML changes.
adoptedCallback()
adoptedCallback() {
console.log("Element moved to a new document");
this.reinitializeForDocument();
}
adoptedCallback() is less common. It runs when an
element is moved into a new document, such as through
document.adoptNode(). Use it when document-specific
setup must be repeated.
constructor vs connectedCallback
| Method | When | Best For |
constructor() | Element instance is created | Shadow root, default fields, templates |
connectedCallback() | Element enters the DOM | Render, listeners, DOM-dependent setup |
Avoid DOM-dependent work in the constructor. The element may not
be connected yet, and the constructor can run before attributes are
fully applied.
Full Lifecycle Component Example
class LiveFeed extends HTMLElement {
static get observedAttributes() {
return ["interval"];
}
constructor() {
super();
this._timer = null;
}
connectedCallback() {
this.render();
this.startTimer();
}
disconnectedCallback() {
this.stopTimer();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === "interval" && oldValue !== newValue) {
this.stopTimer();
this.startTimer();
}
}
startTimer() {
const ms = Number(this.getAttribute("interval")) || 5000;
this._timer = setInterval(() => this.poll(), ms);
}
stopTimer() {
clearInterval(this._timer);
this._timer = null;
}
}
Lifecycle in Lit Components
Lit components extend native lifecycle callbacks with additional
update hooks such as willUpdate(),
updated(), and firstUpdated().
connectedCallback() {
super.connectedCallback();
}
updated(changedProperties) {
if (changedProperties.has("open")) {
this.handleOpenChange();
}
}
firstUpdated() {
this.focusDialog();
}
In Lit, always call super in lifecycle methods so the
base class can manage rendering correctly.
When Lifecycle Callbacks Matter
- You need to render when the element enters the DOM.
- You add event listeners or observers on connect.
- You must clean up resources on disconnect.
- Attributes drive visible component behavior.
- Components fetch data or start timers after mounting.
- Single-page apps mount and unmount elements frequently.
Common Lifecycle Use Cases
- Initial render in
connectedCallback(). - Timer and polling cleanup in
disconnectedCallback(). - Variant and state updates in
attributeChangedCallback(). - Focus management after first connect or update.
- Lazy data loading when the element becomes visible.
- Reinitialization after document adoption in advanced cases.
Lifecycle Best Practices
- Do setup in
connectedCallback(), not the constructor. - Always clean up in
disconnectedCallback(). - Declare
observedAttributes before watching attributes. - Guard against duplicate setup if connect runs multiple times.
- Compare old and new values before re-rendering.
- Keep lifecycle methods focused and readable.
- Call
super in Lit lifecycle overrides.
Common Lifecycle Mistakes
- Doing DOM work in the constructor.
- Forgetting cleanup when the element is removed.
- Assuming
connectedCallback() runs only once. - Watching attributes without
observedAttributes. - Re-rendering on every attribute callback without checks.
- Starting timers or observers without stopping them later.
- Mixing too much business logic into lifecycle methods.
Key Takeaways
- Custom elements have four standard lifecycle callbacks.
connectedCallback() is for setup and rendering. disconnectedCallback() is for cleanup. attributeChangedCallback() reacts to observed attributes. - Good lifecycle design prevents bugs and memory leaks.
Pro Tip
Pair every setup action in connectedCallback() with a
matching cleanup action in disconnectedCallback().
That habit keeps custom elements safe in dynamic applications.