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.
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 are-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>;
  }
}