LWC – Component not Rerendering when Property Changes

apexlightning-web-components

When changing picklist values in my LWC, an array is populated/updated via apex. I assign the updated information to an array but the component does not rerender and therefore does not call a child component to update the page. It only calls the child component on the initial load.

PARENT HTML

<lightning-accordion class="accordian" allow-multiple-sections-open
    active-section-name={activeSections}>
    <lightning-accordion-section name="Option1" label="Option1">
        <c-child products={arr1}
        </c-child>
    </lightning-accordion-section>
</lightning-accordion>

PARENT JS

@track arr1 = [];
opportunityId;
@track opportunity = {};

connectedCallback() {
    this.productSelection();
}

handleBrandChange(event) {
    this.opportunity.Brand__c = event.detail.value;
    this.productSelection(); // call apex to update values (HTML Ommited)
}

productSelection() {
    Promise.all([
        getProductSelection({
            opportunityId: this.opportunityId,
            brand: this.opportunity.Brand__c 
        }),
    ])
        .then(result => {
            //set values ... 
            this.initializeProducts(result);

        })
        .catch(error => {
            //error handling ...
        })
}

initializeProducts(allProducts) {
    window.console.log('BEFORE this.arr1: ', JSON.parse(JSON.stringify(this.arr1)));

    //updated array information from apex method
    let tempArr1 = [];
    for (let i = 0; i < allProducts.length; i++) {
        if (allProducts[i].Product.Brand__c === 'Option1') {
            tempArr1.push(allProducts[i]);
        }
    }

    this.arr1 = tempArr1;
    window.console.log('AFTER this.arr1: ', JSON.parse(JSON.stringify(this.arr1)));

}

CHILD HTML (Snippit)

<template for:each={researchProducts} for:item="product">
    <tr key={product.Product.Id} class="slds-hint-parent">
        <td data-label="Existing" role="gridcell">
            <lightning-input type="checkbox" checked={product.Existing} disabled>
            </lightning-input>
        </td>
        <td data-label="New/Renewal" role="gridcell">
            <lightning-input data-id={product.Id} type="checkbox" checked={product.NewRenewal} onchange={handleProductSelected}>
            </lightning-input>
        </td>
        <td data-label="Research Products">
            <div class="slds-truncate" title={product.Product.Name}>{product.Product.Name} ({product.Product.ProductCode})</div>
        </td>
    </tr>
    </template>

CHILD JS

@api products;
@track researchProducts = [];
@api type
setupopp;
paymentopp;

connectedCallback() {
    window.console.log('CHILD PRODUCTS: ', JSON.parse(JSON.stringify(this.products)));
    this.paymentopp = this.type.includes('Payment');
    this.setupopp = this.type.includes('Setup');
    for (let i = 0; i < this.products.length; i++) {
        if (this.products[i].Product.Classification__c === 'Research' && this.products[i].Product.Family === 'Yearly Subscription') {
            this.researchProducts.push(this.products[i]);
        }
    }
}

The apex updates a boolean value as seen in the screenshots. You can see that when selecting a picklist value, the "NewRenewal" is true. After updating the arr1 property, it is now false. However the child console statement is never reached and the component is not rerendered to show the latest information from Apex.

I am confused since I am updating a boolean value within an object, within an array, which @track should detect the change and rerender my component.

On Initial Load:

enter image description here

After selecting a picklist value: (Notice no child console.log was produced)

enter image description here

Best Answer

connectedCallback is only called when the component initially is attached to the DOM. Since this method is doing stuff to transform the list, then placing it in researchProducts, it will never be recalculated. Consider using a getter here:

get researchProducts() {
  this.paymentopp = this.type?.includes('Payment');
  this.setupopp = this.type?.includes('Setup');
  return !this.products 
    ? []
    : this.products.filter(
      (product) => product.Product.Classification__c === 'Research' &&
                   product.Family === 'Yearly Subscription'
      );
}

You can also try doing this in a setter, but be aware that not all data may be present on the first load, as properties are not guaranteed to be set in any particular order.

You might also want to consider using a setter for type as well.

_type
@api set type(value) {
  this.paymentopp = value?.includes('Payment');
  this.setupopp = value?.includes('Setup');
  this._type = value;
}
get type() {
  return _type;
}
_products;
@api set products(value) {
  this.researchProducts = (value || []).filter(
  (product) => product.Product.Classification__c === 'Research' &&
               product.Family === 'Yearly Subscription'
  );
  this._products = value;
}
get products() {
  return this._products;
}