Web component gotcha: constructor vs connectedCallback

A common mistake I see in web components is this:

class MyComponent extends HTMLElement {
  constructor() {
    super()
    setupLogic()
  }
  disconnectedCallback() {
    teardownLogic()
  }
}

This setupLogic() can be just about anything – subscribing to a store, setting up event listeners, etc. The teardownLogic() is designed to undo those things – unsubscribe from a store, remove event listeners, etc.

The problem is that constructor is called once, when the component is created. Whereas disconnectedCallback can be called multiple times, whenever the element is removed from the DOM.

The correct solution is to use connectedCallback instead of constructor:

class MyComponent extends HTMLElement {
  connectedCallback() {
    setupLogic()
  }
  disconnectedCallback() {
    teardownLogic()
  }
}

Unfortunately it’s really easy to mess this up and to not realize that you’ve done anything wrong. A lot of the time, a component is created once, inserted once, and removed once. So the difference between constructor and connectedCallback never reveals itself.

However, as soon as your consumer starts doing something complicated with your component, the problem rears its ugly head:

const component = new MyComponent()  // constructor

document.body.appendChild(component) // connectedCallback
document.body.removeChild(component) // disconnectedCallback

document.body.appendChild(component) // connectedCallback again!
document.body.removeChild(component) // disconnectedCallback again!

This can be really subtle. A JavaScript framework’s diffing algorithm might remove an element from a list and insert it into a different position in the list. If so: congratulations! You’ve been disconnected and reconnected.

Or you might call appendChild() on an element that’s already appended somewhere else. Technically, the DOM considers this a disconnect and a reconnect:

// Calls connectedCallback
containerOne.appendChild(component)

// Calls disconnectedCallback and connectedCallback
containerTwo.appendChild(component)

The bottom line is: if you’re doing something in disconnectedCallback, you should do the mirror logic in connectedCallback. If not, then it’s a subtle bug just lying in wait for the right moment to strike.

Note: See also “You’re (probably) using connectedCallback wrong” by Hawk Ticehurst, which offers similar advice.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.