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 takes the index and the current item as arguments and needs to return the unique identifier for this item. Now when you change the collection, Angular can track which items have been added or removed according to the unique identifier and create or destroy only the items that changed. That's all.
The trackBy used to improve the performance of the angular project. It is usually not needed only when your application running into performance issues. The angular ngFor directive may perform poorly with large applications.
So angular does not know whether it is old objects collection or not and that's why it destroys old list and then recreates them. This can cause a problem when we are dealing with a large number of objects or list and performance issues will arise. So to avoid this we can use trackBy with ngFor directive.
Angular came up with the trackBy directive, which is optionally passed into ngFor to help identify unique items in our arrays. TrackBy and ngFor together allow Angular to detect the specific node element that needs to change or be added instead of rebuilding the whole array.
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:
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;
}
<input/>
in the control just above your *ngFor
loop - (Yes - just an empty text box)trackBy
has nothing to do with your underlying issue).<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.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.
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;
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With