[SalesForce] Changed value in Parent not updated in child component in LWC

I've below two components: Parent & Child. I'm getting array of objects(having two properties) in Parent component and passing that to child component as two individual fields (city & open).

helloParent.html

<template>
    <lightning-card title="Parent-Child Component Composition">
        <div class="slds-m-around_medium">
            <lightning-button variant="brand" label="Get List" title="Get List of Cities" onclick={getCityList}
                class="slds-m-left_x-small"></lightning-button>
            <h1>This is Parent Component</h1>
            <div class="slds-box slds-box_xx-small">
                <lightning-layout multiple-rows="true">
                    <template for:each={cityList} for:item="city">
                        <lightning-layout-item key={city.city} size="12">
                            <c-hello-child-component city={city.city} open={city.open}>
                            </c-hello-child-component>
                        </lightning-layout-item>
                    </template>
                </lightning-layout>
            </div>
        </div>
    </lightning-card>
</template>

helloParent.js

import { LightningElement, track } from 'lwc';

export default class HelloParentComponent extends LightningElement {
    @track cityList
    getCityList() {
        this.cityList = ['London', 'Paris', 'New York', 'Mumbai', 'Sydney'];
        this.cityList = this.randomizeArray(this.cityList);
        this.cityList = this.cityList.map(city => ({ 'city': city, 'open': false }))
        console.log(this.cityList);
    }

    randomizeArray(arr) {
        const randomArr = arr;
        // Randomize the array
        for (let i = randomArr.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * i);
            const temp = randomArr[i];
            randomArr[i] = randomArr[j];
            randomArr[j] = temp;
        }
        return randomArr;
    }
}

Here two field values are displayed

helloChildComponent.html

<template>
    <div onclick={makeTrue}>
        {city} - {open}
    </div>
</template>

I've method in child component where I can update the value of one field i.e. open. False is passed from parent. This method makes it as true.

helloChildComponent.js

import { api, LightningElement } from 'lwc';
export default class HelloChildComponent extends LightningElement {
    @api city;
    @api open;

    makeTrue() {
        if (!this.open) {
            this.open = true;
        }
    }
}

Issue is happening when I'm again clicking 'Get List' button in parent and again the randomized list of cities along with 'open' property value (which is default false) is created and passed to child. However, the value of open field is not updated on child. It still shows the true (if it was updated in child component).

Best Answer

The reason why this happens, is that updating the child's @api property within the child doesn't inform the parent of the change (it still thinks that open is false).

Consequently, when the parent's cityList notices it has been changed, it causes a render cycle, which compares the old values that were in cityList previously to the new values that are in cityList.

Since false === false, the child component's property isn't updated, because no change was detected in the parent's data.

Instead, set the value from the parent:

// in helloChildComponent.js
makeTrue() {
    this.dispatchEvent(new CustomEvent('open',{detail:{city:this.city}}))
}

<!-- in c-hello-parent-component.html -->
<c-hello-child-component onopen={openCity} city={city.city} open={city.open}>

// helloParentComponent.js
openCity(event) {
    this.cityList.find(item => item.city === event.detail.city).open = true
}

Demo


This ensures that elements that have changed are tracked at the parent level, and the children will update accordingly.