Hello Trailblazers,
In this post we'll understand different lifecycle hooks of lightning web components.
What is a Lifecycle Hook?
What are the different lifecycle hooks of lightning web components?
- constructor()
- connectedCallback()
- renderedCallback()
- disconnectedCallback()
- errorCallback()
- constructor() called on parent
- connectedCallback() called on parent
- constructor() called on child
- connectedCallback() called on child
- renderedCallback() called on child
- renderedCallback() called on parent
- constructor() called on parent
- connectedCallback() called on parent
- constructor() called on child
- connectedCallback() called on child
- errorCallback() called on parent
- renderedCallback() called on child
- renderedCallback() called on parent
constructor()
- You cannot access public properties (i.e. the properties annotated with @api) within a component's constructor. If you try to refer anything, it'll come as undefined
- You cannot access any html tag/element inside the component's constructor as the component is not rendered yet.
- You can access private properties of a component inside a constructor
child.js
import { LightningElement, api } from 'lwc'; export default class Child extends LightningElement { count = 0; @api message = 'default'; constructor() { super(); console.log('Child constructor called'); console.log(this.count); console.log(this.message); console.log(this.template.querySelector('lightning-button')); } }
child.html
<template> <lightning-button label="Increase Count" onclick={increaseCount}></lightning-button> <br /><br /> </template>
connectedCallback()
- connectedCallback() on the parent component will be called before connectedCallback() on the child component is called. This is the reason that you cannot access child components from connectedCallback() of parent, as the child components are not inserted into the dom yet
- This callback is invoked after all the public properties are set. This means that whatever initial values are being passed to the public properties of component, the component will have those values assigned when the connectedCallback() is called
- This also means that connectedCallback() can only access the INITIAL VALUES OF PUBLIC PROPERTIES i.e. if you're updating the public property after the component is inserted, connectedCallback() will not be called again with the new value of public property. So, if you've a method which should be called based on the value of public property, it's better to call that method in a setter instead of connectedCallback() as the setter will be called again and again whenever the value of public property is set/updated
- You can perform some initialization tasks in the connectedCallback() like: listening for events or getting some initial data from the server
- connectedCallback() can be called more than 1 time as well. An example scenario can be: when a component is removed from the DOM and inserted again
child.js
import { LightningElement, api } from 'lwc'; export default class Child extends LightningElement { count = 0; @api message = 'default'; constructor() { super(); console.log('Child constructor called'); console.log(this.count); console.log(this.message); console.log(this.template.querySelector('lightning-button')); } connectedCallback() { console.log('Child connected callback called'); console.log(this.count); console.log(this.message); console.log(this.template.querySelector('lightning-button')); } }As you can see above, I'm trying to access the private property count, public property message and lightning-button again in my connectedCallback(), if you remember in our constructor, we were only able to access the value of private property i.e. count, message was coming as undefined and lightning-button reference was coming as null. Let's see if we're able to access anything else now out of these.
parent.html
<template> <c-child message="hello"></c-child> </template>As you can see above, we're passing the value of message variable as hello. Let's take a look at the js file as well
parent.js
import { LightningElement } from 'lwc'; export default class Parent extends LightningElement { constructor() { super(); console.log('Parent constructor called'); } connectedCallback() { console.log('Parent connected callback called'); console.log(this.template.querySelector('c-child')); } renderedCallback() { console.log('Parent rendered callback called'); console.log(this.template.querySelector('c-child')); } }
- Parent constructor() is called
- Parent connectedCallback() is called and the reference to child component c-child is coming as null inside it
- Child constructor() is called. Value of count is 0, message is undefined and reference to lightning-button is coming as null as it's further a child component for our child lwc.
- Child connectedCallback() is called where value of count is 0. Notice that the value of message is hello instead of default this time because this is the value which is passed from our parent lwc to child lwc for this public property, reference to lightning-button is still coming as null
- Finally, our renderedCallback() method is called in our parent component as per the order and as it's called after the child's connectedCallback() method, this means that the child lwc is now connected to the DOM. Therefore, reference to child lwc is not coming as null this time, as it was coming in the parent's connectedCallback() method.
renderedCallback()
- renderedCallback() on the child component is called before it's called on the parent component
- Whenever the component renders, the expressions used in a template are re-evaluated. This means that if we've created a getter method which is used in our html file and that getter is returning a dynamic value based on some properties, it'll be re-evaluated as well
- Whenever the component's state is changed, the component will be re-rendered
- When a component is re-rendered, the lwc engine attempts to reuse the existing elements. For example: if you update something in the parent component due to which the parent re-renders, it'll not re-render the child component. Another example can be: if you're displaying a list of child components and if you re-order the list, then although the components are placed at different positions now, they're not re-rendered. The engine assumes that the same components are being used and just placed at a different positions now, so they're not re-rendered
However, if you use a different key/update the key of child component, it might get re-rendered - I'm not going to show a demo of this, this is your homework. Try and let me know how it works in the comments down below! - As I specified in point 3, whenever the component's state is changed, the component will be re-rendered. Therefore, we need to make sure that we don't update the state of the component (for example: a property which is being displayed in component's html) in the renderedCallback() itself as it'll re-render the component and will call renderedCallback() again. In this case, the renderedCallback() will be called again and again recursively which will result in an infinite loop
child.js
import { LightningElement, api } from 'lwc'; export default class Child extends LightningElement { count = 0; @api message = 'default'; constructor() { super(); console.log('Child constructor called'); console.log(this.count); console.log(this.message); console.log(this.template.querySelector('lightning-button')); } connectedCallback() { console.log('Child connected callback called'); console.log(this.count); console.log(this.message); console.log(this.template.querySelector('lightning-button')); } renderedCallback() { console.log('Child rendered callback called'); console.log(this.template.querySelector('lightning-button')); } }
- Parent constructor called
- Parent connected callback called where reference to child lwc is coming as null
- Child constructor called where count is 0, message is undefined and reference to lightning-button is coming as null
- Child connected callback called where count is 0, message is hello and reference to lightning-button is again coming as null
- Child rendered callback called where reference to lightning-button is coming properly as the lightning-button is connected to the DOM now
- At last, parent rendered callback is called where reference to child lwc is coming properly as the child lwc is connected to DOM now
increaseCount() { this.dispatchEvent(new CustomEvent('increasecount', { detail: { message: 'Increased count to ' + (++this.count) } })); }
child.js
import { LightningElement, api } from 'lwc'; export default class Child extends LightningElement { count = 0; @api message = 'default'; constructor() { super(); console.log('Child constructor called'); console.log(this.count); console.log(this.message); console.log(this.template.querySelector('lightning-button')); } connectedCallback() { console.log('Child connected callback called'); console.log(this.count); console.log(this.message); console.log(this.template.querySelector('lightning-button')); } renderedCallback() { console.log('Child rendered callback called'); console.log(this.template.querySelector('lightning-button')); } increaseCount() { this.dispatchEvent(new CustomEvent('increasecount', { detail: { message: 'Increased count to ' + (++this.count) } })); } }
parent.html
<template> <lightning-card title={message}> <p class="slds-var-p-around_small"> <c-child onincreasecount={updateMessage} message="hello"></c-child> </p> </lightning-card> </template>
updateMessage(event) { this.message = event.detail.message; }
parent.js
import { LightningElement, track } from 'lwc'; export default class Parent extends LightningElement { message = 'Updated count will appear here!'; constructor() { super(); console.log('Parent constructor called'); } connectedCallback() { console.log('Parent connected callback called'); console.log(this.template.querySelector('c-child')); } renderedCallback() { console.log('Parent rendered callback called'); console.log(this.template.querySelector('c-child')); } updateMessage(event) { this.message = event.detail.message; } }
disconnectedCallback()
disconnectedCallback() { console.log('Child disconnected callback called'); }
child.js
import { LightningElement, api } from 'lwc'; export default class Child extends LightningElement { count = 0; @api message = 'default'; constructor() { super(); console.log('Child constructor called'); console.log(this.count); console.log(this.message); console.log(this.template.querySelector('lightning-button')); } connectedCallback() { console.log('Child connected callback called'); console.log(this.count); console.log(this.message); console.log(this.template.querySelector('lightning-button')); } renderedCallback() { console.log('Child rendered callback called'); console.log(this.template.querySelector('lightning-button')); } disconnectedCallback() { console.log('Child disconnected callback called'); } increaseCount() { this.dispatchEvent(new CustomEvent('increasecount', { detail: { message: 'Increased count to ' + (++this.count) } })); } }
parent.html
<template> <lightning-card title={message}> <p class="slds-var-p-around_small"> <template if:true={show}> <c-child onincreasecount={updateMessage} message="hello"></c-child> </template> <lightning-button label="Toggle Child" onclick={toggleChild}></lightning-button> </p> </lightning-card> </template>
As you can see above, I've added a template tag with if:true condition which is checking a boolean variable named show. Only when this variable is true, our child component will be displayed to us. I'm going to create this variable in our parent.js file. I've added another lightning-button with label Toggle Child which is calling the toggleChild() when clicked. On click of this button, I'm going to toggle the value of show variable from true -> false or from false -> true which will hide/show the child lwc component. This will utlimately call our disconnectedCallback() on our child lwc as well. Let's take a look at the updated parent.js now:
parent.js
import { LightningElement, track } from 'lwc'; export default class Parent extends LightningElement { message = 'Updated count will appear here!'; show = true; constructor() { super(); console.log('Parent constructor called'); } connectedCallback() { console.log('Parent connected callback called'); console.log(this.template.querySelector('c-child')); } renderedCallback() { console.log('Parent rendered callback called'); console.log(this.template.querySelector('c-child')); } updateMessage(event) { this.message = event.detail.message; } toggleChild() { this.show = !this.show; } }As you can see above, I've added show variable below message variable whose default value is true. I've also added another method named toggleChild() at the end. This method will be called when we click the Toggle Child lightning button and it'll toggle the value of show variable from true to false and from false to true.
errorCallback()
child.js
import { LightningElement, api } from 'lwc'; export default class Child extends LightningElement { count = 0; @api message = 'default'; constructor() { super(); console.log('Child constructor called'); console.log(this.count); console.log(this.message); console.log(this.template.querySelector('lightning-button')); } connectedCallback() { console.log('Child connected callback called'); console.log(this.count); console.log(this.message); console.log(this.template.querySelector('lightning-button')); let error = { code: 100, message: 'Error from child connected callback!' }; throw error; } renderedCallback() { console.log('Child rendered callback called'); console.log(this.template.querySelector('lightning-button')); } disconnectedCallback() { console.log('Child disconnected callback called'); } errorCallback(error, stack) { console.log('Child error callback called, error = ' + JSON.stringify(error) + ', stack = ' + JSON.stringify(stack)); } increaseCount() { this.dispatchEvent(new CustomEvent('increasecount', { detail: { message: 'Increased count to ' + (++this.count) } })); } }
connectedCallback() { console.log('Child connected callback called'); console.log(this.count); console.log(this.message); console.log(this.template.querySelector('lightning-button')); let error = { code: 100, message: 'Error from child connected callback!' }; throw error; }
errorCallback(error, stack) { console.log('Child error callback called, error = ' + JSON.stringify(error) + ', stack = ' + JSON.stringify(stack)); }
- error: This is the JavaScript native error object. It's the error which was thrown by component where it occured. In our case it'll be the error object we're throwing which is having two properties: code and message.
- stack: This is a string specifying - in which component the error occured. It'll show path from the component whose errorCallback() was called till the child component where error was thrown
errorCallback(error, stack) { console.log('Parent error callback called, error = ' + JSON.stringify(error) + ', stack = ' + stack); console.log(this.template.querySelector('c-child')); }
parent.js
import { LightningElement, track } from 'lwc'; export default class Parent extends LightningElement { message = 'Updated count will appear here!'; show = true; constructor() { super(); console.log('Parent constructor called'); } connectedCallback() { console.log('Parent connected callback called'); console.log(this.template.querySelector('c-child')); } renderedCallback() { console.log('Parent rendered callback called'); console.log(this.template.querySelector('c-child')); } errorCallback(error, stack) { console.log('Parent error callback called, error = ' + JSON.stringify(error) + ', stack = ' + stack); console.log(this.template.querySelector('c-child')); } updateMessage(event) { this.message = event.detail.message; } toggleChild() { this.show = !this.show; } }
- Only the parent errorCallback() is called and not the errorCallback() present in the child lwc
- Error object is received in the errorCallback() which is exactly the same as thrown by the child lwc. The stack string received in the errorCallback() is showing the stack/path from the parent lwc (the component whose errorCallback() is called) to child lwc (where the error was thrown) as: <c-parent> <c-child>
- I am trying to display a reference to child lwc in the errorCallback() as well and it's working fine. This means that once the child lwc is connected to the DOM it can be referred in any of the callback methods be it errorCallback() or renderedCallback()
- Parent: constructor() called
- Parent: connectedCallback() called (reference to child lwc is null)
- Child: constructor() called (count is coming as 0, message as undefined and reference to lightning-button is coming as null)
- Child: connectedCallback() called (count is coming as 0, message as hello and reference to lightning-button is still null as lightning-button is not connected to DOM yet). This callback method is also throwing error now
- Parent: errorCallback() called (child lwc can now be referenced as it's now connected to DOM)
- Child: renderedCallback() called (reference to lightning-button is coming properly now as lightning-button is now connected to DOM)
- Parent: renderedCallback() called (child lwc can now be referenced here as well because it's now connected to DOM)
This is super post. Loved reading it and the knowledge gained from this post is so clear. Very nicely written. Glad I cam across this post.
ReplyDelete