[SalesForce] How to pass a mutable Array from Parent Component to Child Component in LWC

I want to pass an Array of Objects from a parent to child component. The child component will modify this array (but not perform a DML). This is what i tried

somewhere in parent.js

@track tempList = [];
connectedCallback(){
        getCourseModules({courseId:this.recordId})
        .then(result =>{
            this.moduleList = result;
            this.moduleList.forEach(item=>{
                var a = {};
                a.id = item.LM__c;
                a.navigate = item.LM_c;
                a.label = item.ModName__c;
                a.order = item.Order__c;
                this.tempList.push(a);
            });
        })
        .catch(error => {
            console.error(error);
        })
    }

Somewhere in parent.html

<template>
    <c-child title="Modules" item-list={tempList} ></c-child>
</template>

Somewhere in child.js

    @api
    set itemList(value){
        this.privateItemList = value;
      //shallow copy the array
        this.list = [...value];
    }
    get itemList(){
        return this.privateItemList;
    }

Somewhere in child.html

<template iterator:item={list}>
  <tr key={item.value.id}>
  <td>{item.value.order}</td>
  <td><lightning-button label={item.value.label} data-index={item.value.navigate} variant="base" onclick={linkClick}></lightning-button></td>
</template>

I don't see anything rendering in the child component.

Workaround

//shallow copy the array in parent.js like this
 this.moduleList = result;
 this.moduleList.forEach(item=>{
      var a = {};
      a.id = item.LM__c;
      a.navigate = item.LM_c;
      a.label = item.ModName__c;
      a.order = item.Order__c;
      this.tempList.push(a);
 });
 this.tempList = JSON.parse(JSON.stringify(this.tempList));

//shallow copy the array in child.js like this

    @api
    set itemList(value){
        this.privateItemList = value;
      //shallow copy the array
        this.list = JSON.parse(JSON.stringify(value));
    }

This is working. I wanted to understand-

  1. If this approach of JSON.parse + JSON.stringify might break with future releases of LWC (is it stable enough ?)
  2. Is there a more stable way to passing arrays (that need to be mutated by the child)
  3. Will this approach cause performance issues ? I hope not, because i'm creating a copy of my original Array and not a reference.

Best Answer

If this approach of JSON.parse + JSON.stringify might break with future releases of LWC (is it stable enough ?)

Yes, it's perfectly normal JS, although not the most efficient. However, a point to note, this is a deep copy, not a shallow copy.

Is there a more stable way to passing arrays (that need to be mutated by the child)

The array is wrapped behind a read-only ObservableMembrane, so you have to make a copy. A simpler way to write this could be:

this.privateItemList = value.map(item => {...item});

This copies the array elements and duplicates the items within (one level only), so a shallow copy of the indices. This should work for most trivial cases, just be aware that nested objects would still be read-only.

There's no need to copy in the parent, because even then, the parent would be passing a read-only copy to the child (this happens at the moment the object is passed in via attribute, after you've already made the copy).

Will this approach cause performance issues ? I hope not, because i'm creating a copy of my original Array and not a reference.

There is a performance cost for a copy, but this is usually minor, even with tens of thousands of entries. JS is very efficient at what it does.


But what about an alternative?

The usual method for this type of update is to send an event to the parent to notify it of changes. The parent can then make the changes in its variable, and those changes propagate back to the child. This allows you to still pass by reference, which avoids all the copying and GC (garbage collection) associated with making copies for every change. Consider designing a system where the child notifies the parent, and the parent makes the relevant changes.


<!-- parent.html -->
<c-child onchange={handleChange} ...>

// parent.js
handleChange(event) {
  Object.assign(this.tempList[event.detail.index], event.detail.changes);
}

// child.js
notifyParentOfChange(event) {
  let index = /* calculate index of row to modify */
  let changes = /* calculate changed value, e.g. { SomeField__c: someValue } */
  this.dispatchEvent(new CustomEvent('change', { detail: { index, changes } }));
}
Related Topic