简体   繁体   中英

angular ngFor trackBy does not work as I expected

When I read the doc ( https://angular.io/api/common/NgForOf ) for ngFor and trackBy , I thought I understood that Angular would only redo the DOM if the value returned by the trackBy function is changed, but when I played with it here ( https://stackblitz.com/edit/angular-playground-bveczb ), I found I actually don't understand it at all. Here's the essential part of my code:

export class AppComponent {
  data = [
    { id: 1, text: 'one' },
    { id: 2, text: 'two' },
    { id: 3, text: 'three' },
  ];

  toUpper() {
    this.data.map(d => d.text = d.text.toUpperCase());
  }

  trackByIds (index: number, item: any) {
    return item.id; 
  };
}

And:

<div *ngFor="let d of data; trackBy: trackByIds">
  {{ d.text }}
</div>
<button (click)=toUpper()>To Upper Case</button>

What I expected was clicking the button should NOT change the list from lower case to upper, but it did. I thought I used the trackByIds function for the trackBy in the *ngFor , and since the trackByIds only checks the id property of the items, so the change of anything other than id should not cause the DOM to be redone. I guess my understanding is wrong.

The trackBy function determines when a div element created by the ngFor loop should be re-rendered (replaced by a new element in the DOM). Please note that Angular can always update an element on change detection by modifying its properties or attributes. Updating an element does not imply replacing it by a new one. That is why setting the text to uppercase is reflected in the browser, even when the div elements are not re-rendered.

By default, without specifying a trackBy function, a div element will be re-rendered when the corresponding item value changes. In the present case, that would be when the data array item is replaced by a different object (the item "value" being the object reference); for example after executing the following method:

recreateDataArray() {
  this.data = this.data.map(x => Object.assign({}, x));
}

Now, with a trackBy function that returns the data item id , you tell the ngFor loop to re-render the div element when the id property of the corresponding item changes. Therefore, the existing div elements would remain in the DOM after executing the recreateDataArray method above, but they would be replaced by new ones after running the following method:

incrementIds() {
  this.data.forEach(x => { x.id += 10; });
}

You can experiment with this stackblitz . A checkbox allows to turn on/off the trackByIds logic, and a console message indicates when the div elements have been re-rendered. The "Set Red Text" button changes the style of the DOM elements directly; you know that red div elements have been re-rendered when their content turns to black.

If trackBy doesn't seem to work:

1) Make sure you are using the correct signature for the trackBy function

https://angular.io/api/core/TrackByFunction

interface TrackByFunction<T> {
  (index: number, item: T): any
}

Your function must take an index as the first parameter even if you're only using the object to derive the 'tracked by' expression.

trackByProductSKU(_index: number, product: { sku: string })
{
    // add a breakpoint or debugger statement to be 100% sure your
    // function is actually being called (!)
    debugger;
    return product.sku;
}

2) Make sure the entire control (that contains the *ngFor) isn't being redrawn, possibly as a side effect of something else.

  • Add <input/> in the control just above your *ngFor loop - (Yes - just an empty text box)
  • Load the page and type something into each of the textboxes (I usually just enter 1, 2, 3, 4...)
  • Add / remove an item from the list - or whatever you need to do to trigger a change
  • If the contents of your textbox disappears then it means you're redrawing the entire container control (in other words your trackBy has nothing to do with your underlying issue).
  • You can put <input/> at each 'level' if you have multiple nested loops. Just type a value into each box, then see which values are retained when you perform whatever action is causing the problem.

3) Make sure the trackBy function is returning a unique value for each row:

<li *ngFor="let item of lineItems; trackBy: trackByProductSKU">

    <input />
    Tracking value: [{{ trackByProductSKU(-1, item) }}]
</li>

Display the track by value inside your loop like this. This will eliminate any stupid mistakes - such as getting the name or casing of the track by property incorrect. The empty input element is deliberate

If everything is working properly you should be able to type in each input box, trigger a change in the list and it shouldn't lose the value you type.

4) If you cannot determine a unique value, just return the item itself. This is the default behavior (from trackByIdentity ) if you don't specify a trackBy function.

// angular/core/src/change_detection/differs/default_iterable_differ.ts

const trackByIdentity = (index: number, item: any) => item;

export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChanges<V> {

 constructor(trackByFn?: TrackByFunction<V>) {
    this._trackByFn = trackByFn || trackByIdentity;
 }

5) Don't accidentally return null or undefined!

Let's say you're using product: { sku: string } as your trackBy function and for whatever reason the products no longer have that property set. (Maybe it changed to SKU or has an extra level.)

If you return product.sku from your function and it's null then you're going to get some unexpected behavior.

trackBy is actually used to prevent rerendering of same element in the DOM again and again. It cannot be used like you're using. Let me elaborate. If you got array like:

data = [
{ id: 1, text: 'one' },
{ id: 2, text: 'two' },
{ id: 3, text: 'three' },


];

And on click of button you change the array somewhat like:

  changeArray() {


this.data = [
    { id: 1, text: 'one' },
    { id: 2, text: 'two' },
    { id: 3, text: 'three' },
    { id: 4, text: 'four' },
    { id: 5, text: 'five' },
  ];
  }

trackByIds (index: number, item: any) {
return item.id; 
  };
}

And:

<div *ngFor="let d of data; trackBy: trackByIds">
  {{ d.text }}
</div>
<button (click)="changeArray()>Change Array</button>

When you call changeArray() on click of button. Then, trackBy guarantees that only items having new id will be added to DOM and previous one's will not be rerendered ie items having id 1-3 wil not be rendered again in DOM. If you think you can use it to prevent manipulations, then you're wrong. Hope you get me! Proof: Before button click在单击 changeArray 之前 After button click, only highlighted content changes

按钮单击后仅显示突出显示的四个和五个,因为它们只是更改

If one is converted to ONE using click of button 证明一是否转换为一

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM