salesforce / lwc

⚡️ LWC - A Blazing Fast, Enterprise-Grade Web Components Foundation
https://lwc.dev
Other
1.64k stars 393 forks source link

@track annotation doesn't work when changing a reference of an inner array. #2553

Closed kgrzywacz closed 3 years ago

kgrzywacz commented 3 years ago

Description

While having an array(with @track annotation) with following structure:

[
  {
    innerArray: ['item1']
  }
]

if we change reference of innerArray inside the object this change will not be tracked / reflected in the UI.

As far as I understand the documentation @track when applied to array or object should detect any changes in inner arrays and objects of tracked element. In this case (check code below) I'm changing the reference of inner array but change isn't reflected in UI.

Steps to Reproduce

See link below.

  1. Click on "Populate Value" button to generate tab
  2. Click on "Swap Items" button to swap item.

https://webcomponents.dev/edit/k1QUihllBQdbSYk4m3G9/src/app.js

import { LightningElement, track } from "lwc";

export default class App extends LightningElement {
  values
  @track
  valuesUI

  constructor() {
    super();
    this.values = [];
    this.valuesUI = [];
  }

  handleSwap() {
    this.values.forEach((val)=>{
      val.changePlain();
    });
  }

  populateValues() {
    this.values.push(new Value())
    for(const v of this.values) {
      this.valuesUI.push(v.contructPlainValue())
    }
  }
}

let valCounter = 0;

class Value {
  constructor() {
    this.id = valCounter++;
    this.name = `Value ${valCounter}`;
    this.items = new Map([
      ["one", ["item1"]], ["two", ["item2"]]
      ]);
      this.currentItems = "one";
  }
  contructPlainValue() {
    this.plainObject = {id: this.id, name: this.name, items: this.items.get(this.currentItems)}
    return this.plainObject;
  }
  changePlain() {
    if(this.currentItems ==="one"){
      this.currentItems = "two";
    } else {
      this.currentItems = "one";
    }
    this.plainObject.items = this.items.get(this.currentItems);
  }
}
<template>
    <div class="app slds-p-around_x-large">
        <lightning-tabset>
            <template for:each={valuesUI} for:item="val">
                <lightning-tab label={val.name} key={val.id}>
                    <template for:each={val.items} for:item="itm">
                        <span key={itm}>{itm}</span>
                    </template>
                </lightning-tab>
            </template>
        </lightning-tabset>
        <lightning-button variant="brand" label="Populate Value" onclick={populateValues} class="slds-m-left_x-small">
        </lightning-button>
        <lightning-button variant="brand" label="Swap items" onclick={handleSwap} class="slds-m-left_x-small">
        </lightning-button>
    </div>
</template>

Expected Results

Value in span should change from item1 to item2.

Actual Results

Nothing happens

Browsers Affected

Chrome 95.0.4638.54, Firefox 93.0

Version

Possible Solution

Additional context/Screenshots Add any other context about the problem here. If applicable, add screenshots to help explain.

jodarove commented 3 years ago

@kgrzywacz, thanks for the detailed repro. Unfortunately, this use case is not supported, and stated in the documentation; check the last part of the track section :

However, the framework doesn't observe mutations made to complex objects, such as objects inheriting from Object, class instances, Date, Set, or Map.

I'll close this issue as it is working as intended.

kgrzywacz commented 3 years ago

@jodarove Thanks for quick reply. I am aware of this limitation and it's not the case in this situation. Instance of the class Value is used to store all variables, create a plain javascript object (that is pushed to a tracked array) and later modify this plain object using stored references, it is not tracked.

In the App class (that extends LightningElement) you can see that there are fields

jodarove commented 3 years ago

correct, however, they are not modified from the one in component (valuesUI, which is a proxy that tracks mutations), they are modified as part of the Value instance, and the framework can't keep track of those changes.

kgrzywacz commented 3 years ago

Hm, my intuition was (since both valuesUI array and Value instances contain reference to the same object) that no matter through which reference I will modify it, it will be detected since in the end both reference the same place in memory.

How does lwc tracks changes then? Does it check which references were accessed in code?

jodarove commented 3 years ago

Ah, that's key. They do not hold the same reference; the track value (valueUI) stores a proxy to the real object (from Value). When you read valueUI (via the proxy), the engine detects that the view depends on those values, and when you modify it (via the proxy also), the engine is notified that the view is out of date re-render the component. But if you change the object itself, there's no proxy in the middle; therefore, the engine does not detect that the view is out of date, and the component needs to re-render.

You can take a look at @track fields logic in the installed descriptor. which internally uses the observable-membrane project.

kgrzywacz commented 3 years ago

Thanks @jodarove for detailed explanation. Do you think it would make sense to add this information to lwc documentation?