Reactivity

Note

Reactivity changed significantly in the Lightning Web Components Open Source 1.1.0 release.

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.

Tip

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.

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.

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.

The firstName and lastName fields contain primitives.

firstName = '';
lastName = '';

Note

Fields are reactive. Expandos, which are properties added to an object at runtime, are not reactive.

Reactivity Considerations

Although fields are reactive, the LWC engine tracks field value changes in a shallow fashion. Changes are detected when a new value is assigned to the field by comparing the value identity using ===. This works well for primitive types like numbers or boolean.

import { LightningElement } from 'lwc';

export default class ReactivityExample extends LightningElement {
  bool = true;
  number = 42;
  obj = { name: 'John' };

  checkMutation() {
    this.bool = false;   // Mutation detected
    
    this.number = 42; // No mutation detected: previous value is equal to the newly assigned value
    this.number = 43; // Mutation detected
    
    this.obj = { name: 'John' }; // Mutation detected: redefining the object with the same value creates a new object so it's not ===
    this.obj.name = 'Bob'; // No mutation detect: `obj` field value is not reassigned
    this.obj = { ...this.obj, title: 'CEO' } // Mutation detected
  }  
}

When manipulating complex types like objects and arrays, you must create a new object and assign it to the field for the change to be detected.

To avoid such issues when working with complex objects, use the @track decorator to deeply tracks mutations made to the field value.

Track Changes Inside Objects and Arrays

Decorate the field with @track to observe changes to the properties of an object or to the elements of an array.

When a field is decorated with @track, Lightning Web Components tracks changes to the internal values of:

The framework observes mutations made to plain objects and arrays in a recursive fashion, including nested objects, nested arrays, and a mix of objects and arrays. Cyclic references are also handled.

However, the framework doesn't observe mutations made to complex objects, such as objects inheriting from Object, class instances, Date, Set, or Map.

Observe an Object's Properties

To tell the framework to observe changes to the properties of an object, decorate the field with @track.

Note

As discussed earlier, without using @track, the framework observes changes that assign a new value to the field. If the new value is not === to the previous value, the component rerenders.

Let's declare the fullName field, which contains an object with two properties. The framework observes changes that assign a new value to the fullName 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.

Rerender an Object with New Properties

A component rerenders only if a property accessed during the previous rendering cycle is updated, even when the object is annotated with @track. This prevents the component from rerendering excessively,

Consider this tracked object and a getter that prints the object’s properties.

@track obj = {value1: 'Hello'};

get words() {
    return Object.entries(this.obj)
              .map(([key, value]) => ({key, value}));
  }

During the first render cycle, the framework records that obj.value1 is accessed. Any mutation to obj that doesn’t affect value1 is ignored since it doesn’t impact the rendered content. Therefore, a change to value1 triggers a rerendering, but adding a new property to obj or a change to value2 doesn’t trigger a rerendering.

// Component rerenders.
setValue1(e) {
    this.obj.value1 = 'Hello World';
}

// Component doesn’t rerender.
setValue2(e) {
    this.obj.value2 = 'Hello LWC';
}

To rerender your component when adding a new property, assign the object to a new object with both values.

setValue2(e) {
    this.obj = {
        ...this.obj,
        value2: 'Hello LWC'
    };
}

Observe an Array's Elements

Another use case for @track is to tell the framework to observe changes to the elements of an array.

If you don’t use @track, the framework observes changes that assign a new value to the field.

arr = ['a','b'];

The component rerenders when you assign a new value to arr.

// Component rerenders.
this.arr = ['x','y','z'];

However, if we update or add an element in the array, the component doesn’t rerender.

// Component doesn’t rerender.
this.arr[0] = 'x';
this.arr.push('c');

To tell the framework to observe changes to the array’s elements, decorate the arr field with @track Additionally, the framework doesn’t automatically convert the array to a string for updates to the array’s elements. To return the updated string, use a getter to convert the elements of the array to a string using join().

@track arr = ['a','b'];

get computedArray() { return this.arr.join(','); }

update() {
    this.arr[0] = 'x';
    this.arr.push('c');
}

Observe Complex Objects

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.

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.

To ensure that the template is rerendered when the value changes, clone the existing date and update its value.

Note

When you set a property to a value that can’t be tracked, a warning is logged. If you’re trying to debug a scenario where the component isn’t rerendering on change, look in your browser console. For our example, the browser console logs this helpful warning:

Property "x" of [object:vm TrackDate] is set to a non-trackable object,
which means changes into that object cannot be observed.