Skip to main content
Version: Next

Reactive Data

Stencil components update when props or state on a component change.

Rendering methods

When a props or state change on a component, the render() method is scheduled to run.

The Watch Decorator (@Watch())

@Watch() is a decorator that is applied to a method of a Stencil component. The decorator accepts a single argument, the name of a class member that is decorated with @Prop() or @State(). A method decorated with @Watch() will automatically run when its associated class member changes.

// We import Prop & State to show how `@Watch()` can be used on
// class members decorated with either `@Prop()` or `@State()`
import { Component, Prop, State, Watch } from '@stencil/core';

@Component({
tag: 'loading-indicator'
})
export class LoadingIndicator {
// We decorate a class member with @Prop() so that we
// can apply @Watch()
@Prop() activated: boolean;
// We decorate a class member with @State() so that we
// can apply @Watch()
@State() busy: boolean;

// Apply @Watch() for the component's `activated` member.
// Whenever `activated` changes, this method will fire.
@Watch('activated')
watchPropHandler(newValue: boolean, oldValue: boolean) {
console.log('The old value of activated is: ', oldValue);
console.log('The new value of activated is: ', newValue);
}

// Apply @Watch() for the component's `busy` member.
// Whenever `busy` changes, this method will fire.
@Watch('busy')
watchStateHandler(newValue: boolean, oldValue: boolean) {
console.log('The old value of busy is: ', oldValue);
console.log('The new value of busy is: ', newValue);
}

@Watch('activated')
@Watch('busy')
watchMultiple(newValue: boolean, oldValue: boolean, propName:string) {
console.log(`The new value of ${propName} is: `, newValue);
}
}

In the example above, there are two @Watch() decorators. One decorates watchPropHandler, which will fire when the class member activated changes. The other decorates watchStateHandler, which will fire when the class member busy changes.

When fired, the @Watch()'ed method will receive the old and new values of the prop/state. This is useful for validation or the handling of side effects.

info

The @Watch() decorator does not fire when a component initially loads.

Handling Arrays and Objects

When Stencil checks if a class member decorated with @Prop() or @State() has changed, it checks if the reference to the class member has changed. When a class member is an object or array, and is marked with @Prop() or @State, in-place mutation of an existing entity will not cause @Watch() to fire, as it does not change the reference to the class member.

Updating Arrays

For arrays, the standard mutable array operations such as push() and unshift() won't trigger a component update. These functions will change the content of the array, but won't change the reference to the array itself.

In order to make changes to an array, non-mutable array operators should be used. Non-mutable array operators return a copy of a new array that can be detected in a performant manner. These include map() and filter(), and the spread operator syntax. The value returned by map(), filter(), etc., should be assigned to the @Prop() or @State() class member being watched.

For example, to push a new item to an array, create a new array with the existing values and the new value at the end:

import { Component, State, Watch, h } from '@stencil/core';

@Component({
tag: 'rand-numbers'
})
export class RandomNumbers {
// We decorate a class member with @State() so that we
// can apply @Watch(). This will hold a list of randomly
// generated numbers
@State() randNumbers: number[] = [];

private timer: NodeJS.Timer;

// Apply @Watch() for the component's `randNumbers` member.
// Whenever `randNumbers` changes, this method will fire.
@Watch('randNumbers')
watchStateHandler(newValue: number[], oldValue: number[]) {
console.log('The old value of randNumbers is: ', oldValue);
console.log('The new value of randNumbers is: ', newValue);
}

connectedCallback() {
this.timer = setInterval(() => {
// generate a random whole number
const newVal = Math.ceil(Math.random() * 100);

/**
* This does not create a new array. When stencil
* attempts to see if any Watched members have changed,
* it sees the reference to its `randNumbers` State is
* the same, and will not trigger `@Watch` or a re-render
*/
// this.randNumbers.push(newVal)

/**
* Using the spread operator, on the other hand, does
* create a new array. `randNumbers` is reassigned
* using the value returned by the spread operator.
* The reference to `randNumbers` has changed, which
* will trigger `@Watch` and a re-render
*/
this.randNumbers = [...this.randNumbers, newVal]
}, 1000)
}

disconnectedCallback() {
if (this.timer) {
clearInterval(this.timer)
}
}

render() {
return(
<div>
randNumbers contains:
<ol>
{this.randNumbers.map((num) => <li>{num}</li>)}
</ol>
</div>
)
}
}

Updating an object

The spread operator should be used to update objects. As with arrays, mutating an object will not trigger a view update in Stencil. However, using the spread operator and assigning its return value to the @Prop() or @State() class member being watched will. Below is an example:

import { Component, State, Watch, h } from '@stencil/core';

export type NumberContainer = {
val: number,
}

@Component({
tag: 'rand-numbers'
})
export class RandomNumbers {
// We decorate a class member with @State() so that we
// can apply @Watch(). This will hold a randomly generated
// number.
@State() numberContainer: NumberContainer = { val: 0 };

private timer: NodeJS.Timer;

// Apply @Watch() for the component's `numberContainer` member.
// Whenever `numberContainer` changes, this method will fire.
@Watch('numberContainer')
watchStateHandler(newValue: NumberContainer, oldValue: NumberContainer) {
console.log('The old value of numberContainer is: ', oldValue);
console.log('The new value of numberContainer is: ', newValue);
}

connectedCallback() {
this.timer = setInterval(() => {
// generate a random whole number
const newVal = Math.ceil(Math.random() * 100);

/**
* This does not create a new object. When stencil
* attempts to see if any Watched members have changed,
* it sees the reference to its `numberContainer` State is
* the same, and will not trigger `@Watch` or are-render
*/
// this.numberContainer.val = newVal;

/**
* Using the spread operator, on the other hand, does
* create a new object. `numberContainer` is reassigned
* using the value returned by the spread operator.
* The reference to `numberContainer` has changed, which
* will trigger `@Watch` and a re-render
*/
this.numberContainer = {...this.numberContainer, val: newVal};
}, 1000)
}

disconnectedCallback() {
if (this.timer) {
clearInterval(this.timer)
}
}

render() {
return <div>numberContainer contains: {this.numberContainer.val}</div>;
}
}