DEV Community

Chris Haynes
Chris Haynes

Posted on • Originally published at lamplightdev.com on

When are the constructor and connectedCallback methods called when creating a Custom Element?

The constructor and connectedCallback lifecycle methods of Custom Elements are called when your element is created and attached to the DOM respectively, but there are a few subtleties to keep in mind depending on how and when you define, create, insert and declare your element.

So how are Custom Elements defined, created, inserted and declared? And at which stage do the lifecycle methods get called?

Define

A custom element is defined when customElements.define is called:

class MyElement extends HTMLElement {
    constructor() {
        super();

        console.log('constructed');
    }

    connectedCallback() {
        console.log('connected');
    }
}

customElements.define('my-element', MyElement);
Enter fullscreen mode Exit fullscreen mode

Defining an element doesn't trigger either the constructor or the connectedCallback methods since it does not create an instance of an element

An element can only be defined once.

Create

An element can be created in JavaScript in two ways:

// can happen before definition
const myElement = document.createElement('my-element');
Enter fullscreen mode Exit fullscreen mode
// can only happen if already defined
const myElement = new MyElement();
Enter fullscreen mode Exit fullscreen mode

Creation triggers the constructor, if the element has already been defined.

The constructor is only ever called once per element instance.

Insert

An element is inserted into the DOM imperatively with JS:

document.body.append(myElement);
Enter fullscreen mode Exit fullscreen mode

Insertion triggers the connectedCallback method, if the element has already been defined.

connectedCallback is called each time the element is inserted into the DOM, with the disconnectedCallback method called each time it is removed from the DOM.

Declare

An element is declared when parsed as HTML:

<my-element></my-element>
Enter fullscreen mode Exit fullscreen mode
document.body.innerHTML = '<my-element></my-element>';
Enter fullscreen mode Exit fullscreen mode

Declaration triggers the constructor and connectedCallback methods, if the element has already been defined.

Upgrade

In all the above cases the lifecycle methods are only called if the element has already been defined. An element is upgraded when it already exists before definition - at the point of definition the constructor is then called automatically. If the element was already attached to the DOM at this point connectedCallback will also be called.

Examples

The examples below cover the different orders of the above stages to illustrate when the lifecycle methods are called.

Define then Declare

<html>
    <head>
        <script>
            class MyElement extends HTMLElement {
                constructor() {
                    super();

                    console.log('constructed');
                }

                connectedCallback() {
                    console.log('connected');
                }
            }

            customElements.define('my-element', MyElement);
        </script>
    </head>
    <body>
        <my-element></my-element>
        <!-- `constructor` then `connectedCallback` are called here when the element has been parsed -->
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

 Declare then Define

<html>
    <head>
    </head>
    <body>
        <my-element></my-element>

        <script>
            class MyElement extends HTMLElement {
                constructor() {
                    super();

                    console.log('constructed');
                }

                connectedCallback() {
                    console.log('connected');
                }
            }

            // UPGRADE
            customElements.define('my-element', MyElement);
            /**
            `constructor` then `connectedCallback` are called here when the element has been defined -->
            **/
        </script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Define then Create then Insert

<html>
    <head>
    </head>
    <body>
        <script>
            class MyElement extends HTMLElement {
                constructor() {
                    super();

                    console.log('constructed');
                }

                connectedCallback() {
                    console.log('connected');
                }
            }

            customElements.define('my-element', MyElement);

            const myElement = document.createElement('my-element');
            /**
            `constructor` called here when element is created
            **/

            document.body.appendChild(myElement);
            /**
            `connectedCallback` called here when element is inserted
            **/
        </script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

 Create then Insert then Define

<html>
    <head>
    </head>
    <body>
        <script>
            const myElement = document.createElement('my-element');

            document.body.appendChild(myElement);

            class MyElement extends HTMLElement {
                constructor() {
                    super();

                    console.log('constructed');
                }

                connectedCallback() {
                    console.log('connected');
                }
            }

            // UPGRADE
            customElements.define('my-element', MyElement);
            /**
            `constructor` then `connectedCallback` are called here when the element has been defined -->
            **/
        </script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

 Create then Define then Insert

<html>
    <head>
    </head>
    <body>
        <script>
            const myElement = document.createElement('my-element');

            class MyElement extends HTMLElement {
                constructor() {
                    super();

                    console.log('constructed');
                }

                connectedCallback() {
                    console.log('connected');
                }
            }

            // UPGRADE
            customElements.define('my-element', MyElement);
            /**
            `constructor` called here when the element has been defined
            **/

            document.body.appendChild(myElement);
            /**
            `connectedCallback` called here when the element has been defined -->
            **/
        </script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Why does any of this matter?

Knowing when, how and why the constructor and connectedCallback methods are called is important when initialising your Custom Elements. Generally:

  • the constructor is best suited to setting up initial state and events that don't need to be removed or cleaned up when the element is destroyed (as there is no deconstructor method.)

  • the constructor is only ever called once per element so initialisation that needs to happen each time the element is attached to the DOM should be deferred to connectedCallback.

  • the connectedCallback method is best suited to most other initialisation tasks. Any clean up can then happen in disconnectedCallback.

  • attributes and child elements should not be accessed or modified in the constructor since, depending on how the element is created, they may not exist e.g. document.createElement('my-element'); - this will trigger the constructor but the element has had no chance yet to set attributes or children.

  • if you need to access / modify attributes or children on initialisation this must therefore happen in connectedCallback. In this case you will often need to guard against such initialisation happening multiple times as the element is removed and re-attached to the DOM.

Subscribe to my mailing list to be notified of new posts about Web Components and building performant websites

Top comments (0)