Search code examples
webcomponentsshadow-domlit-element

Lit web component not updated on attribute change


I'm changing an attribute of a Lit web component, but the changed value won't render.

I have an observed array: reports[] that will be populated in firstUpdated() with reports urls fetched from rest apis. The loading of the array is done by:

this.reports.push({ "name" : report.Name, "url" : this.apiUrl + "/" + report.Name + "?rs:embed=true" });

see below:

import { LitElement, html, css } from 'lit';
import {apiUrl, restApiUrl} from '../../config';

export default class Homepage extends LitElement {
  static properties = {
    apiUrl: '',
    restApiUrl: '',
    reports: []
  }

...

  constructor() {
    super();

    this.apiUrl = apiUrl;
    this.restApiUrl= restApiUrl;
    this.reports = []; 
  }

  firstUpdated() {
    ...
    // Fetch all reports from restApiUrl:
    rsAPIDetails(restApiUrl).then(reports =>{     
      for(const report of reports.value)
      {       
        rsAPIDetails(restApiUrl + "(" + report.Id + ")/Policies").then(policies => {
          for(const policy of policies.Policies)
          {
            if(policy.GroupUserName.endsWith(usernamePBI))
            {
              for(const role of policy.Roles)
              {
                if(role != null && (role.Name== "Browser" || role.Name== "Content Manager")) 
                {
                  // User has access to this report so i'll push it to the list of reports that will show in the navbar:
                  this.reports.push({ "name" : report.Name, "url" : this.apiUrl + "/" + report.Name + "?rs:embed=true" });
                }
              }
            }
          }
        });
      }
    }).then(q => {
      console.log(this.reports);
    });
  }

  render() {
    return html`
      <div id="sidenav" class="sidenav">
        ...
        <div class="menucateg">Dashboards</div>
        ${this.reports.map((report) =>
          html`<a @click=${() => this.handleMenuItemClick(report.url)}>${report.name}</a>`
        )}
        <div class="menucateg">Options</div>
      </div>
    `;
  }

At console I can clearly see that the array is loaded with the correct values. But the render() function won't update the web component with the new values of reports[]: The links should be added inside 'Dashboards' div

If instead I statically populate reports[] with values (in the ctor), it renders the links just fine.

So why isn't the component updated when the observed array is changed ?

Thank you!


Solution

  • Array.push mutates the array, but doesn't change the actual value in memory.

    To have LitElement track updates to arrays and objects, the update to the value needs to be immutable.

    For example, we can make your example work by doing it this way:

    const newReports = this.reports.slice();
    newReports.push({ "name" : report.Name, "url" : this.apiUrl + "/" + report.Name + "?rs:embed=true" });
    this.reports = newReports;
    

    Or by using array spread

    this.reports = [...this.reports, { "name" : report.Name, "url" : this.apiUrl + "/" + report.Name + "?rs:embed=true" }]
    

    The reason why this works is that when you do this.reports.push(), you're not actually changing the "reference" of this.reports, you're just adding an object to it. On the other hand, when you re-define the property with this.reports = ..., you are changing the "reference", so LitElement knows the value has changed, and it will trigger a re-render.

    This is also true for objects. Let's say you have a property obj. If you updated the object by just adding a property to, the element wouldn't re-render.

    this.obj.newProp = 'value';
    

    But if you re-define the object property as a whole by copying the object and adding a property, it will cause the element to update properly.

    this.obj = {...this.obj, newProp: 'value'}
    

    You can see the values that are getting tracked and updated by using the updated method.