When are the constructor and connectedCallback methods called when creating a Custom Element?
Mon Mar 15 2021
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);
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');
// can only happen if already defined
const myElement = new MyElement();
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);
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>
document.body.innerHTML = '<my-element></my-element>';
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>
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>
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>
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>
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>
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 nodeconstructor
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 toconnectedCallback
.the
connectedCallback
method is best suited to most other initialisation tasks. Any clean up can then happen indisconnectedCallback
.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 theconstructor
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.