How to force an update of a subcomponent?

Viewed 102

Lit Documentation says:

When called with no arguments, requestUpdate() schedules an update, without calling a hasChanged() function. But note that requestUpdate() only causes the current component to update. That is, if a component uses the code shown above, and the component passes this.myArray to a subcomponent, the subcomponent will detect that the array reference hasn't changed, so it won't update.

So how to force an update of a subcomponent (even if the subcomponent's attribute doesn't change)?

render() {
  return html`
    <ul>
      ${this.myArray.map(
        (item) => html`
          <li>
            ${item.text}
            <my-subcomponent foo="${item.bar}"></my-subcomponent>
          </li>
        `
      )}
    </ul>
  `;
}

Edit: Repro in Lit Playground

1 Answers

Thank you for the lit.dev playground repro, I was able to identify some issues that should resolve your issue.

Before jumping into the various fixes, why is the subcomponent not re-rendering? LitElements render as a function of their properties & state. If properties don't change, they don't re-render.

The subcomponent has two reactive properties, myArray and id. myArray is only set once on construction from localstorage and never updated - so it will never trigger an update. The id number also never changes for a given subcomponent so will never trigger an update. Because neither properties change, the subcomponent doesn't re-render.

Option 1: Pass the new array to the sub-component.

Fix 1 in Lit Playground

The only change I've made is to pass myArray to sub-component explicitly, (note .myArray=${this.myArray}):

<li>${item.text} (<sub-component id=${item.id} .myArray=${this.myArray}></sub-component>)</li>

This works because now when the parent updates this.myArray, the parent passes this new array to the sub-component. The sub-component then notices that the reactive property myArray has changed and a render is scheduled.

Option 2: Only pass the item to the sub-component

This is a larger change, but more maintainable and simpler. Instead of passing both the array and item id to each sub-component, only pass the item that the sub-component cares about.

This does less work on the sub-component because each sub-component doesn't need to loop through the whole array to find the element that it cares about.

Working fixed playground from repro.

This approach requires a change in changeMyArray. Instead of mutating the items, new items need to be returned.

  changeMyArray() {
    this.myArray = this.myArray.map(
      (item) => {
        return {...item, colored: !item.colored};
      }
    );
  }

Breaking down that example. this.myArray.map already returns a new array so the array spread is not required. Within the map each item that is modified must return a new object. This is similar to how redux works and the linked article may provide more helpful details.

Now the parent render function can be updated to pass the item directly to the subcomponent with: <li>${item.text} (<sub-component .item=${item}></sub-component>)</li>.

Now when the item changes, the sub-component automatically re-renders.

Option 3: Manually calling a method that triggers an update

I would not recommend this over option 2, but wanted to show how you could trigger the sub-component to re-render.

Working sample: https://lit.dev/playground/#gist=fb38e52bc4d35dd74485407eb19db84f

There are two changes. In the parent, 'main-component', changeMyArray method I've added a loop that calls a method on each sub-component:

this.shadowRoot.querySelectorAll('sub-component')
        .forEach(component => component.refresh())

And on the sub-component I've defined refresh as:

  refresh() {
    this.myArray = JSON.parse(localStorage.getItem('myArray'));
  }

Because the sub-component now has the new version of this.myArray from localstorage it will trigger a re-render.

The cons of this approach and why option 1 or 2 is better:

  • Each sub-component needs to manually parse the array out of localstorage.
  • This approach requires manually managing when renders happen instead of letting the state flow down through properties.

There's a video called Event communication between web-components by Elliott on the Lit team that discusses state and events further.

Related