Reactive properties in LWC : All fields are reactive. If the value of a field changes and the field is used in a template or in the getter of a property used in a template, the component re-renders and the renderedCallback()
lifecycle hook is called. When a component re-renders, all the expressions used in the template are re-evaluated.
To make a field public and therefore available to a component’s consumers as a property, decorate it with @api
.
Field and property are almost interchangeable terms. A component author declares fields in a class. An instance of the class has properties, so to component consumers, a field is a property. In a Lightning web component, only fields that a component author decorates with @api
are publicly available to consumers as object properties.
Decorators are a JavaScript language feature. The @api
and @track
decorators are unique to Lightning Web Components. A field can have only one decorator.
Public Property Reactivity
To expose a public property, decorate a field with @api
. Public properties define the API for a component. An owner component that uses the component in its HTML markup can access the component’s public properties via HTML attributes.
If the value of a public property changes, the component’s template re-renders.
todoApp.js
Let’s look at a simple app in the playground. The example-todo-item
component has a public itemName
property. The example-todo-app
component consumes example-todo-item
and sets its property via the item-name
attribute.
Property names in JavaScript are in camel case while HTML attribute names are in kebab case (dash-separated) to match HTML standards. In the example-todo-app
template, the item-name
attribute on example-todo-item
maps to the itemName
JavaScript property on example-todo-item
.
In todoApp.html, change the value of the item-name
attributes and watch the component re-render.
import { LightningElement, api } from 'lwc'; export default class TodoApp extends LightningElement {}
todoApp.html
<template> <example-todo-item item-name="Milk"></example-todo-item> <example-todo-item item-name="Bread"></example-todo-item> </template>
todoItem.js
import { LightningElement, api } from 'lwc'; export default class TodoItem extends LightningElement { @api itemName; }
todoItem.html
<template> <div>{itemName}</div> </template>
Let’s walk through the code.
The TodoItem
class imports the @api
decorator from lwc
. It declares an itemName
field and decorates it with @api
to make it public. This class is part of the example-todo-item
component, where example
is the namespace.
// todoItem.js import { LightningElement, api } from 'lwc'; export default class TodoItem extends LightningElement { @api itemName = 'New Item'; }
The component’s template defines a single todo item.
<!-- todoItem.html --> <template> <div> <label>{itemName}</label> </div> </template>
A parent component, in this case example-todo-app
, can set the itemName
property on child example-todo-item
components.
<!-- todoApp.html --> <template> <div> <example-todo-item item-name="Milk"></example-todo-item> <example-todo-item item-name="Bread"></example-todo-item> </div> </template>
The parent component can also access and set the itemName
property in JavaScript.
// todoApp.js const myTodo = this.template.querySelector('example-todo-item'); myTodo.itemName // New Item
Field Reactivity
All fields are reactive. When the framework observes a change to a field used in a template or used in the getter of a property used in a template, the component re-renders.
In this example, the firstName
and lastName
fields are used in the getter of the uppercasedFullName
property, which is used in the template. When either field value changes, the component re-renders.
helloExpressions.js
import { LightningElement } from 'lwc'; export default class HelloExpressions extends LightningElement { firstName = ''; lastName = ''; handleChange(event) { const field = event.target.name; if (field === 'firstName') { this.firstName = event.target.value; } else if (field === 'lastName') { this.lastName = event.target.value; } } get uppercasedFullName() { return `${this.firstName} ${this.lastName}`.trim().toUpperCase(); } }
helloExpressions.html
<template> <div> <ui-input name="firstName" label="First Name" onchange={handleChange} ></ui-input> <ui-input name="lastName" label="Last Name" onchange={handleChange} ></ui-input> <p class="margin-top-medium"> Uppercased Full Name: {uppercasedFullName} </p> </div> </template>
input.js
import { LightningElement, api, track } from 'lwc'; export default class Input extends LightningElement { @api set checked(val) { this._checked = val; } get checked() { return this._checked; } @api disabled; @api hasClearButton; @api label; @api max; @api min; @api name; @api type = 'text'; @api set value(val) { this.valuePrivate = this._value = val !== undefined ? val : ''; } get value() { return this._value; } @track valuePrivate = ''; _checked = false; changeHandler() { this._checked = !this._checked; this.dispatchEvent(new CustomEvent('change')); } keyupHandler(event) { this._value = event.target.value; this.dispatchEvent(new CustomEvent('change')); } handleClearClick() { this._value = ''; this.dispatchEvent(new CustomEvent('change')); } get isCheckboxField() { return this.type === 'checkbox'; } get isNumberField() { return this.type === 'number'; } get isSearchField() { return this.type === 'search'; } get isTextField() { return this.type === 'text' || this.type === 'search'; } get calculatedClassFormElement() { let classSet = this.getAttribute('class'); if (!classSet) { classSet = ['slds-form-element__control']; } else { classSet = classSet.split(' '); classSet.push('slds-form-element__control'); } if (this.type === 'search') { classSet.push('slds-input-has-icon slds-input-has-icon_left-right'); } return classSet.join(' '); } get calculateClassInput() { let classSet = [this.type !== 'checkbox' ? 'slds-input' : 'slds-checkbox']; if (this.disabled) { classSet.push('slds-is-disabled'); } return classSet.join(' '); } }
input.html
<template> <div> <template if:false={isCheckboxField}> <label>{label}</label> </template> <div class={calculatedClassFormElement}> <template if:false={isCheckboxField}> <template if:true={isSearchField}> <svg aria-hidden="true"> <use xlink:href="/assets/icons/utility-sprite/svg/symbols.svg#search" ></use> </svg> </template> <input class={calculateClassInput} type={type} value={valuePrivate} onkeyup={keyupHandler} onchange={changeHandler} /> <template if:true={hasClearButton}> <button title="Clear" onclick={handleClearClick} > <svg aria-hidden="true" > <use xlink:href="/assets/icons/utility-sprite/svg/symbols.svg#clear" ></use> </svg> <span>Clear</span> </button> </template> </template> <template if:true={isCheckboxField}> <div class="slds-checkbox"> <!-- TODO: Checkbox not longer working --> <input class={calculateClassInput} type="checkbox" value={valuePrivate} onchange={changeHandler} /> <label> <span></span> <span>{label}</span> </label> </div> </template> </div> </div> </template>
The firstName
and lastName
fields contain primitives.
firstName = ''; lastName = '';
There is a limit to the depth of changes that the framework observes. If a field contains an object value or an array value, the framework observes changes that assign a new value to the field. If the value that you assign to the field is not ===
to the previous value, the component re-renders. To tell the framework to observe changes to the properties of an object or to the elements of an array, decorate the field with @track
.
Let’s declare the fullName
field, which contains an object with two properties. The framework observes changes that assign a new value to the field.
fullName = { firstName : '', lastName : ''};
This code changes the value of fullName
, so the component re-renders.
this.fullName = { firstName: 'Jane', lastName: 'Doe' };
This code changes the value of firstName
. The framework is not observing changes to firstName
, so the component doesn’t re-render. Remember, the framework is observing changes to the fullName
field. This code doesn’t assign a new value to fullName
, instead it assigns a value to the firstName
property.
this.fullName.firstName = 'Jane';
To tell the framework to observe changes to the properties of the fullName
object, decorate the field with @track
.
@track fullName = { firstName: '', lastName: '' };
Now when the firstName
property changes, the component re-renders.
this.fullName.firstName = 'Jane';
Let’s look at this code in the playground. Enter a first name and last name and watch the component re-render. Now remove @track
and do the same. The component doesn’t re-render.
helloExpressionsTrack.js
import { LightningElement, track } from 'lwc'; export default class HelloExpressionsTrack extends LightningElement { @track fullName = { firstName: '', lastName: '' }; handleChange(event) { const field = event.target.name; if (field === 'firstName') { this.fullName.firstName = event.target.value; } else if (field === 'lastName') { this.fullName.lastName = event.target.value; } } get uppercasedFullName() { return `${this.fullName.firstName} ${this.fullName.lastName}`.trim().toUpperCase(); } }
helloExpressionsTrack.js
<template> <div> <ui-input name="firstName" label="First Name" onchange={handleChange} ></ui-input> <ui-input name="lastName" label="Last Name" onchange={handleChange} ></ui-input> <p class="margin-top-medium"> Uppercased Full Name: {uppercasedFullName} </p> </div> </template>
input.js
import { LightningElement, api, track } from 'lwc'; export default class Input extends LightningElement { @api set checked(val) { this._checked = val; } get checked() { return this._checked; } @api disabled; @api hasClearButton; @api label; @api max; @api min; @api name; @api type = 'text'; @api set value(val) { this.valuePrivate = this._value = val !== undefined ? val : ''; } get value() { return this._value; } @track valuePrivate = ''; _checked = false; changeHandler() { this._checked = !this._checked; this.dispatchEvent(new CustomEvent('change')); } keyupHandler(event) { this._value = event.target.value; this.dispatchEvent(new CustomEvent('change')); } handleClearClick() { this._value = ''; this.dispatchEvent(new CustomEvent('change')); } get isCheckboxField() { return this.type === 'checkbox'; } get isNumberField() { return this.type === 'number'; } get isSearchField() { return this.type === 'search'; } get isTextField() { return this.type === 'text' || this.type === 'search'; } get calculatedClassFormElement() { let classSet = this.getAttribute('class'); if (!classSet) { classSet = ['slds-form-element__control']; } else { classSet = classSet.split(' '); classSet.push('slds-form-element__control'); } if (this.type === 'search') { classSet.push('slds-input-has-icon slds-input-has-icon_left-right'); } return classSet.join(' '); } get calculateClassInput() { let classSet = [this.type !== 'checkbox' ? 'slds-input' : 'slds-checkbox']; if (this.disabled) { classSet.push('slds-is-disabled'); } return classSet.join(' '); } }
input.html
<template> <div> <template if:false={isCheckboxField}> <label>{label}</label> </template> <div class={calculatedClassFormElement}> <template if:false={isCheckboxField}> <template if:true={isSearchField}> <svg aria-hidden="true"> <use xlink:href="/assets/icons/utility-sprite/svg/symbols.svg#search" ></use> </svg> </template> <input class={calculateClassInput} type={type} value={valuePrivate} onkeyup={keyupHandler} onchange={changeHandler} /> <template if:true={hasClearButton}> <button title="Clear" onclick={handleClearClick} > <svg aria-hidden="true" > <use xlink:href="/assets/icons/utility-sprite/svg/symbols.svg#clear" ></use> </svg> <span>Clear</span> </button> </template> </template> <template if:true={isCheckboxField}> <div class="slds-checkbox"> <!-- TODO: Checkbox not longer working --> <input class={calculateClassInput} type="checkbox" value={valuePrivate} onchange={changeHandler} /> <label> <span></span> <span>{label}</span> </label> </div> </template> </div> </div> </template>
Reactive Data Types
There is a limit to the depth of changes that the framework observes. The depth depends on the data type of the field.
Note
Fields are reactive. Expandos are not reactive.
Lightning Web Components observes changes to the internal values of these types of fields:
- Primitive values
- Plain objects created with
{…}
and decorated with@track
- Arrays created with
[]
and decorated with@track
If a field contains a primitive data type, reactivity simply works as expected.
If a field contains an object value or an array value, it’s important to understand how the framework tracks changes. As we saw in Field Reactivity, the framework doesn’t observe changes to the properties of an object or to the elements of an array unless the field is decorated with @track
.
Let’s look at a component with a field, x
, of type Date
. The template has a few buttons that change the internal state of x
. This example highlights that new Date()
creates an object, but not via {}
, so the internals of the object aren’t observed, even though the code uses @track
.
trackDate.js
import { LightningElement, track } from 'lwc'; export default class TrackDate extends LightningElement { @track x; initDate() { this.x = new Date(); } updateDate() { this.x.setHours(7); } }
tackDate.html
<template> <p>Date: {x}</p> <button onclick={initDate}>Init</button> <button onclick={updateDate}>Update</button> </template>
When you click the Init button, the x
field is assigned a new Date
object and the template re-renders. However, when you click Update, the template doesn’t re-render because the framework doesn’t track changes to the value of the Date
object.