See how Stencil fits into the entire Ionic Ecosystem ->
Stencil is part of the Ionic Ecosystem ->

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);
  }
}

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>;
  }
}
BackNext
Contributors