The life of a Web Component - Initialization

This post is a quick write-up on some of my learnings in experimenting with Web Components. It focuses mostly on the small subset of their life-cycle methods that deal with initialization.

I've been playing around with Web Components lately. They are quite an exciting thing with a vast range of use cases and possibilities. Web Components provide a very flexible API to explore non-standard approaches and Frankenstein contraptions. I know I will :)

MDN has an excellent intro to Web Components. It explains the three main parts of Web Components in some detail (custom tags, shadow root, and template slots). I recommend you start with MDN if you are looking to take your first steps into this tech.

Three HTML5 badges in a row. The first one has the normal orientation. The second one is upside down. The third one has a random orientation.

Here is a quick explanation if, like me, you are coming from a text-book framework (react, vue, svelte, etc…) and have not done anything too serious with them:

Their life-cycle methods are nuanced and happen at different stages than the common text-book frameworks typically present us with their components.

Just being alive

Here is a small subset of the life-cycle of a Web Component:

After playing for a while with Web Components, here are some personal considerations about their initialization methods.

A super constructor does nothing at all.

The Web Components spec is clear about what a constructor() can do. The limitations are many and entirely optional. It is a guideline when they say, "The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement or createElementNS methods.". It is entirely up to us to abide or not.

Since I'm not too fond of code and rules, I tend to skip adding a constructor in most of my Web Components. I ask myself: "do I need an event listener? Can it be delegated elsewhere? Do I need shadow root encapsulation? What am I going to cook for dinner? If food were Web Components would it need to be constructed?".

Anyway, delegating logic to the caller/parent is the long-honored approach of the lazy dev. Moving some logic to the component or code above usually makes things easier to remove while keeping it more uncomplicated down the hierarchy.

In this regard, the basic Web Component creation template would be something like:

class ImAWebComponent extends HTMLElement {
// no constructor()
}

That's it, no constructor. No need to keep up with the Web Components spec rules and the browser will use the HTMLElement standard constructor.

All these notifications keep distracting me.

Like the constructor approach above, it is better to keep the Web Component disconnected. That means that it is preferable to avoid using the connectedCallback() method when possible.

Why? Because this particular method can be called multiple times throughout the life of a Web Component. Should it be responsible for the initialization of the Web Component? What happens if the components move around after being already initialized? Can the component code handle being "initialized" multiple times on top of each other? Why is this Web Component alive? (why are we all?) Will it have fun during its life-time? Will this Web Component spend its life under its parent's home?

For most autonomous Web Components I find it better to design them with no DOM attachment flow in mind. That means that the simple Web Component template remains the same:

class ImAWebComponent extends HTMLElement {
// no constructor()
// no connectedCallback()
}

Then use a life flow that I am more comfortable with, say something like:

class ImAWebComponent extends HTMLElement {
// no constructor()
// no connectedCallback()

initialize() {
// some initialization logic here
}

update() {
// refresh stuff and keep things revised
}
...

The idea here is to use whatever suits you better as the life-cycle-flow of your components. Web Components have a somewhat low-level API that molds quickly to the kinkiest desires. You can make them work closer to how you like to decompose problems or instead go wild and be creative.

You can avoid classes altogether and use a pure-functional approach that ultimately nails down to the Web Component class like some IO trash-can. Or keep using Object-Oriented Programming and spawn a vast forest of hierarchical extensions. Or use another language, why stick to JavaScript if Web Components are just HTML that can be executed by your favorite language WebView in a declarative way? All good, everything is possible here.

addEventListener("load",
  () => document.querySelectorAll("my-tag")
                .forEach(tag => tag.initialize()))

Calling the initialize() or update() functions can then happen at any moment, controllable by the app. In a render() function? Once at the "load" event? How to use it will largely depend on what kind of component this is and its context.

Web Components can use other Web Components.

As expected, a Web Component can use another Web Component. However, the browser is not always smart enough to delay its creation until the customElements global registry defines the tag for the Web Component that it depends.

A common approach is to use the customElements.whenDefined("my-tag"), which returns a Promise when the tag is defined. Put a bunch of these in a Promise.all array if the Web Component uses other Web Component tags inside it.

class MyComponent extends HTMLElement {
  whenLoaded = Promise.all(
    [
      customElements.whenDefined("dom-loop"),
      customElements.whenDefined("redux-for-blink-tags"),
    ],
  );

  initialize() {
	this.whenLoaded.then(() => {
    // Runs after the `whenLoaded` promise resolves
    });
  }
}

The above pattern guarantees that the "dom-loop" and "redux-for-blink-tags" are availabe inside the initialization promise.

Conclusion

I am having fun playing with Web Components and believe that they provide a fertile ground for experimentation. This post is just a quick intro to a subset of their life-cycle methods and the provided functionality included in browsers that don't need extra JS to exist. I will write about my further investigations with Web Components in the following days.


This post is Part 1 of a series I am writting called "The Life of a Web Component".

The other parts are: