Jens Willmer

Tutorials, projects, dissertations and more..

How StencilJS processes web component lists

I found a strange bug in the representation of a web component list in a StencilJS project the other day. This post covers the reasons of the bug and how I solved it.

This is how my initial web component looked like:

@Component({
  tag: 'app-website',
  styleUrl: 'app-website.scss'
})
export class AppWebsite {
  @Prop() data: IWebsiteStatus;
  @State() isHealthy: boolean = false;

  async componentWillLoad() {
    this.isHealthy = this.data.healthy;
  }

  render() {
    return [
      <div>
        {this.data.title} {this.isHealthy ? `(Healthy)` : `(Unhealthy)`}
      </div>
    ];
  }
}

And the following part is from the parent component that generates the website components:

<ion-list>
  {this.filteredWebsites.map(website =>
    <app-website data={website} />
  )}
</ion-list>

The initial output of two websites looked like this :

Website-1 (Healthy)
Website-2 (Unhealthy)

After an update to the filteredWebsites list that removed Website-1 from the list the output looked like this:

Website-2 (Healthy)

Note: My expectation was that the output would look like the one below. If your agree then read on and I explain why we got a different output and how to fix it.

Website-2 (Unhealthy)

The problem is how StencilJS handles the list of elements. When the filteredWebsites list item order changes (because of the removal of an item in the middle). StencilJS does not remove the web component that has the removed object attached. Instead it just removes the last web component and reapplies the data objects to the existing web components. Since we did not handle the data change event in the example above we did not notice it and the status of the items seams to change then in fact the data of the web components have changed:

# Initial state
component-1 data-1
component-2 data-2

# After removing data-1 from the filteredWebsites list
component-1 data-2

The fix is very simple. Handle the data change event like this:

@Watch("data")
async dataPropertyChanged() {
  this.isHealthy = this.data.healthy;
}