Skip to content

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.