← All articles

In StencilJS, @Prop Does Not Always Trigger a Re-render—Use @Watch Too

Why changing a Stencil @Prop after render does not update the DOM, and how to combine @Watch with @State to trigger re-renders.

Published
In StencilJS, @Prop Does Not Always Trigger a Re-render—Use @Watch Too cover image

Suppose you create a very simple component in Stencil: a button component with a label attribute.

@Component({
  tag: 'my-button',
  styleUrl: './my-button.scss',
  scoped: true
})
export class ButtonComponent {
  @Prop() label: string = '';

  render() {
    return (
      <button>{this.label}</button>
    );
  }
}

You can use it as <my-button label="button"></my-button>. On the initial render, render() runs with the @Prop in place, so the result is naturally displayed as <button>button</button>.

What happens if you change the label attribute in JavaScript after rendering?

const button = document.querySelector('my-button');
button.setAttribute('label', 'my-button')

Even after this change, my-button is not re-rendered. From the perspective of ButtonComponent, only the value of this.label changed, so render() does not run again. Stencil works this way to avoid the cost of watching every @Prop at the value level and to prevent unnecessary re-renders.

First, Stencil provides the @Watch decorator to detect changes. Let us update the component above.

  @Component({
    tag: 'my-button',
    styleUrl: './my-button.scss',
    scoped: true
  })
  export class ButtonComponent {
    @Prop() label: string = '';
+   @Watch('label')
+   async watchLabelHandler() {
+   }
  
    render() {
      return (
        <button>{this.label}</button>
      );
    }
  }

Now, whenever label changes, watchLabelHandler() runs. However, render() still does not run. For render() to run, a property decorated with @State must change, so let us add a @State _label property for re-render tracking.

  @Component({
    tag: 'my-button',
    styleUrl: './my-button.scss',
    scoped: true
  })
  export class ButtonComponent {
    @Prop() label: string = '';
+   @State() _label: string;   
    @Watch('label')
    async watchLabelHandler() {
+     this._label = this.label;
    }
  
    render() {
      return (
        <button>{this.label}</button>
      );
    }
  }

Now, whenever the label attribute changes, @Watch('label') runs watchLabelHandler(), and updating _label inside it triggers a re-render.

Simple, right?

See you next time.