The custom element lifecycle

In the last chapter, we learned that a custom element isn't allowed to manipulate its DOM in the constructor. That work is meant to be deferred to the lifecycle callbacks, which let us know when our element has been attached to the document, which it is removed, and when its attributes are changed. These are just methods that we define in our class, like so:

class LifecycleExample extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    // called when the element is attached to the document
    // similar to React's "componentDidMount"
  }

  disconnectedCallback() {
    // called on removal
    // similar to React's "componentWillUnmount"
  }

  attributeChangedCallback(attribute, previousValue, currentValue) {
    // called when attributes are added, removed, or changed
  }

  static get observedAttributes() {
    return ["src"]
  }

}

Let's talk about each of these in turn.

connectedCallback()

The connectedCallback method is run whenever your element gets attached to the document, either by the HTML parser or by any JavaScript that appends it to a parent element. This is where you probably want to put most of your actual setup code, but be aware that connectedCallback can be run multiple times if your element is moved around. For any one-time setup, be sure to add a guard property to the element to track execution:

connectedCallback() {
  if (!this.initialized) {
    // run expensive first-time setup code
    this.initialized = true;
  }
  // regular connection code can go here
}

In practice, I find that the connectedCallback is not usually a place where I do a lot of heavy work anyway. It's typically where I'll set up observers (of the Intersection or Mutation variety) or sometimes insert a small HTML template. It's also a good place to register for events on document or window. But for a lot of custom elements, this method can be omitted without repercussions.

disconnectedCallback()

As the name implies, this method gets called when your element is disconnected from the document. It's a good place to put any cleanup code: removing observers or event listeners that might have been added in connectedCallback(). Don't assume that this will be called once for each connectedCallback(), either before or after.

attributeChangedCallback()

attributeChangedCallback() is called whenever an attribute is added, removed, or altered on your element. This can happen in several scenarios:

In response to an attribute change notification, you might trigger a resource download, change something about the element's display, or start an animation — anything that you might expect from changing the attribute on a regular element.

Not every attribute will trigger this lifecycle method. In order to get notifications about attribute changes, your element class needs to declare a static observedAttributes getter that returns an array of attribute names. For example, to listen for changes to the "src" and "title" attributes, we'd add this getter to our class:

static get observedAttributes() {
  return [ "src", "title" ]
}

The actual lifecycle method is called with three arguments: the name of the attribute, its previous value (if any) and its current value (if any). The first argument is guaranteed, but depending on the mutation the other two may be either a string or null:

Attribute was... previousValue currentValue
Added null "string"
Changed "old string" "new string"
Removed "string" null

One notable limitation of the attribute system is that it only accepts one type: DOMString (which is, for all intents and purposes, a regular JS string). As useful as strings are, sometimes you want something more complex. It's possible to try to cram other values into an attribute, perhaps using JSON encoding, but it's generally a bad idea. A good rule of thumb is that if you need a non-primitive value (i.e, an object or array), use a setter property or a method as the interface instead of an attribute.

In my opinion, the attributeChangedCallback() is the key to good custom elements. In the next chapter, we'll talk about how to make the most of it.