Reputation: 358
I have an issue with change detection running too often within one of my components, so I'm trying to use the trackBy
option for the ngFor
directive.
Through reading, I understand that Angular will use the value returned from your trackyBy
function for it's diff the next time change detection runs. To see if it fits my needs, and to try and understand it better, I set up a playground. When using it, I set the return value of the trackyBy
function I use to return undefined, and I still got the results I wanted.
TS:
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
styleUrls: ['./app.component.scss'],
templateUrl: './app.component.html',
})
export class AppComponent {
collection;
constructor() {
this.collection = [{id: 1, value: 0}, {id: 2, value: 0}, {id: 3, value: 0}];
}
getItems() {
this.collection = this.getItemsFromServer();
}
getItemsFromServer() {
return [{id: 5, value: 0}, {id: 2, value: 0}, {id: 3, value: 3}, {id: 4, value: 4}];
}
trackByFn(index, item) {
return undefined;
}
}
HTML:
<ul>
<li *ngFor="let item of collection;trackBy: trackByFn">{{ item.id }}hello {{item.value}}</li>
</ul>
<button (click)="getItems()">Refresh items</button>
The results on the first click is, all items rerender with their new value or id, except index 1 of the array. On second click, none of the items rerender bc nothing changes within the objects.
So my question is, why would one ever use a unique id for the return value of trackBy
function? There has to be something I am missing and I'm not wanting it to affect my application in a way I don't see yet.
Upvotes: 1
Views: 3141
Reputation: 7350
TLDR; don't return undefined
or some other constant in your trackBy. It doesn't mean anything and there are better alternatives whatever is your need.
The implementation details of ngFor
are quite complex and I'm not going to dissect them for the sake of this answer. Besides, we really don't need to analyze every single detail. We just need to understand what's the purpose of trackBy
:
trackBy is a mean to ease the tracking of items in the iterable source, in order to (1) optimize performance and (2) prevent destruction of items which could be kept alive.
(1) is obvious: the less DOM manipulation, the better performance is.
(2) not so much: if I want to animate elements in the list (e.g. move elements while animating a sort operation, or showing an :enter
or :leave
animation) I really need that the instantiated components are "properly" tracked. If I don't setup trackBy correctly, the items will be needlessly destroyed and recreated, instead of moved around correctly.
To better show this, I did setup an example to show the point:
https://stackblitz.com/edit/angular-dnh2ti
Each ngFor
generates AppCounter
instances, capable of tracking the cumulative number of constructor calls and displaying a name
value. In other words, we can track if the component is recreated and if the component input is reset.
The first button changes the value of the name
binding. No ngFor
recreates the components, because the items are by default tracked by object instance.
The second button (Create New Instances) shows that even if we change completely the instances, rebuilding the objects in the array from the ground up, we can tell the ngFor
component to manage the component creation in a smarter way. That is: the Counters for complex values - no trackBy recreates all the components at every click on the button; so does the trackByUndefined
, while the trackByProperty
correctly prevents component destruction. This is crucial if you need to animate enter and leave, or if component creation can be costly. This is particularly true for item components with onPush
changeDetectionStrategy
.
The trackByProperty
method is a one liner:
trackByPropertyName(idx: number, value: any) {
// use ?. on new TypeScript versions
return value && value.name;
}
The second button also randomizes the array randomValues
. This is used to show another alternative to the trackByProperty
method: trackByIndex
.
If you really want to keep all the components in the same order they are rendered, and you want only the data to be updated, one simple but effective option is to use:
trackByIndex(idx: number, _value: any) {
return idx;
}
This obviously prevents animations when sorting, but it keeps the instantiated components "alive", even if the value change completely.
This answer is already pretty long, almost a blog post, but I want to reiterate the point: use a trackBy to tell ngFor
what strategy to use to optimize component instantiation. The function is going to be tipically a one liner, just returning an id property or the index of the row, so there is no added complexity in doing so, but it will make the performance and the behavior of your ngFor
s better and a lot more predictable. Use the default only if you have simple values or you don't care about performance at all (but, really, who doesn't?).
Upvotes: 1
Reputation: 13515
The official answer is that you use trackBy
to avoid recreating elements in the DOM for new instances of objects that have the same identifier.
On the face of it, your setup doesn't prove that ngFor
isn't just ignoring the undefined
value you are returning in trackByFn
and recreating the DOM elements anyway when new instances appear in the model.
New items with the same id as existing objects may have had other properties changed, so you would expect the HTML to be correct regardless of whether or not you use (or mis-use) trackBy
.
I created a test environment using your code, except I forked the source code for *ngFor
so that I could add my own logging to trace what happens inside *ngFor
.
I tested three scenarios:
A) trackByFn
returns the unique id
B) trackByFn
returns undefined
C) does't use trackBy
I traced what happens in each scenario for the following steps
I assigned new instances of objects at each step for a "pure" test.
1. create list
The same for all 3 scenarios - a DOM element is created for each item in the list.
2. partially replace some list data
A) removes DOM elements for removed items and creates new DOM elements for new items. All elements are updated.
B) creates new DOM elements for items with an index out of the bounds of the original array. All elements are updated.
C) recreates all DOM elements.
3. sort list
A) moves the DOM elements that have moved positions
B) updates the DOM elements that have moved positions
C) recreates all DOM elements
4. reset list data
A) removes DOM elements for removed items and creates new DOM elements for new items. All elements are updated (same as scenario 2).
B) removes DOM elements for removed items. All elements are updated.
C) recreates all DOM elements.
It is important to note that these tests were done using new instances of objects. *ngFor
is more efficient if you are reusing object references.
Using trackBy
is more efficient in terms of DOM manipulation if you have a very volatile list.
The surprising result
From my tests it appears that your example does less DOM manipulation than when returning the unique identifier from trackByFn
. If you replace 3 items with 3 new items, your method wouldn't do any DOM manipulation and would still run the same update method as the "proper" way. The "proper" method would remove the original 3 DOM elements and add 3 new DOM elements.
This suggests that we could just provide a trackByFn
that returns a constant value without any unexpected results. From having looked through the source code and played around with it, I can't see how this is a problem (aside from confusing other people who look at your code).
This does make me wonder why the default implementation has to recreate all of the DOM elements, when reusing old DOM elements seems to work just fine. I'm sure there are some cases that I've not considered, and I would love to hear them.
DEMO: https://stackblitz.com/edit/angular-anejhw
This turned into a bit of "fun" research task rather than a definitive answer, but hopefully it proves useful. Even though I've shown that returning a constant value from trackByFn
seems to be the most performant option, I'd still be hestitant about using this approach in production code. Even if it works for all cases now I wouldn't be surprised if it were to be "fixed" at some point as a bug.
ngForOf
source code: https://github.com/angular/angular/blob/master/packages/common/src/directives/ng_for_of.ts
Upvotes: 6