As web apps get more complex, we need some way to divide the code into manageable chunks. To do this, we can use Web Components to create reusable UI blocks that we can use in multiple places.
In this article, we’ll look at the lifecycle hooks of Web Components and how we use them.
Lifecycle Hooks
Web Components have their own lifecycle. The following events happen in a Web Component’s lifecycle:
- Element is inserted into the DOM
- Updates when a UI event is being triggered
- Element deleted from the DOM
Web Component has lifecycle hooks are callback functions that capture these lifecycle events and let us handle them accordingly.
They let us handle these events without creating our own system to do so. Most JavaScript frameworks provide the same functionality, but Web Components is a standard so we don’t need to load extra code to be able to use them.
The following lifecycle hooks are in a web component:
constructor()
connectedCallback()
disconnectedCallback()
attributeChangedCallback(name, oldValue, newValue)
adoptedCallback()
constructor()
The constructor()
is called when the Web Component is created. It’s called when we create the shadow DOM and it’s used for setting up listeners and initialize a component’s state.
However, it’s not recommended that we run things like rendering and fetching resources here. The connectedCallback
is better for these kinds of tasks.
Defining a constructor is optional for ES6 classes, but an empty one will be created when it’s undefined.
When creating the constructor, we’ve to call super()
to call the class that the Web Component class extends.
We can have return
statements in there and we can’t use document.write()
or document.open()
in there.
Also, we can’t gain attributes or children in the constructor method.
connectedCallback()
connectedCallback()
method is called when an element is added to the DOM. We can be sure that the element is available to the DOM when this method is called.
This means that we can safely set attributes, fetch resources, run set up code or render templates.
disconnectedCallback()
This is called when the element is removed from the DOM. Therefore, it’s an ideal place to add cleanup logic and to free up resources. We can also use this callback to:
- notify another part of an application that the element is removed from the DOM
- free resources that won’t be garbage collected automatically like unsubscribing from DOM events, stop interval timers, or unregister all registered callbacks
This hook is never called when the user closes the tab and it can be trigger more than once during its lifetime.
attributeChangedCallback(attrName, oldVal, newVal)
We can pass attributes with values to a Web Component like any other attribute:
<custom-element
foo="foo"
bar="bar"
baz="baz">
</custom-element>
In this callback, we can get the value of the attributes as they’re assigned in the code.
We can add a static get observedAttributes()
hook to define what attribute values we observe. For example, we can write:
class CustomElement extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({
mode: 'open'
});
}
static get observedAttributes() {
return ['foo', 'bar', 'baz'];
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`${name}'s value has been changed from ${oldValue} to ${newValue}`);
}
}
customElements.define('custom-element', CustomElement);
Then we should the following from the console.log
:
foo's value has been changed from null to foo
bar's value has been changed from null to bar
baz's value has been changed from null to baz
given that we have the following HTML:
<custom-element foo="foo" bar="bar" baz="baz">
</custom-element>
We get this because we assigned the values to the foo
, bar
, and baz
attributes in the HTML with the values of the same name.
adoptedCallback()
The adoptedCallback
is called when we call document.adoptNode
with the element passed in. It only occurs when we deal with iframes.
The adoptNode
method is used to transfer a node from one document to another. An iframe has another document, so it’s possible to call this with iframe’s document object.
Example
We can use it to create an element with text that blinks as follows:
class BlinkElement extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
const shadow = this.attachShadow({
mode: 'open'
});
this.span = document.createElement('span');
this.span.textContent = this.getAttribute('text');
const style = document.createElement('style');
style.textContent = 'span { color: black }';
this.intervalTimer = setInterval(() => {
let styleText = this.style.textContent;
if (style.textContent.includes('red')) {
style.textContent = 'span { color: black }';
} else {
style.textContent = 'span { color: red }';
}
}, 1000)
shadow.appendChild(style);
shadow.appendChild(this.span);
}
disconnectedCallback() {
clearInterval(this.intervalTimer);
}
static get observedAttributes() {
return ['text'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'text') {
if (this.span) {
this.span.textContent = newValue;
}
}
}
}
customElements.define('blink-element', BlinkElement);
In the connectedCallback
, we have all the code for initializing the element. It includes adding a span
for the text that we passed into the text
attribute as its value, and the styles for blinking the text. The style starts with the creation of the style
element and then we added the setInterval
code to blink the text by changing the color of the span
from red to black and vice versa.
Then we attached the nodes to the shadow DOM which we created at the beginning.
We have the observedAttributes
static method to set the attributes to watch for. This is used by the attributeChangedCallback
to watch for the value of the text attribute and set it accordingly by getting the newValue
.
To see the attributeChangedCallback
in action, we can create the element dynamically and set its attribute as follows:
const blink2 = document.createElement('blink-element');
document.body.appendChild(blink2);
blink2.setAttribute('text', 'bar');
blink2.setAttribute('text', 'baz');
Then we should bar
and baz
if we log newValue
in the attributeChangedCallback
hook.
Finally, we have the disconnectedCallback
which is run when the component is removed. For example, when we remove an element with removeChild
as in the following code:
const blink2 = document.createElement('blink-element');
document.body.appendChild(blink2);
blink2.setAttribute('text', 'bar');
document.body.removeChild(blink2);
The lifecycle hooks allow us to handle various DOM events inside and outside the shadow DOM. Web Components have hooks for initializing, removing, and attribute changes. We can select which attributes to watch for changes.
There’re also hooks for adopting elements in another document.