Reputation: 105439
I can't really understand what I should return from trackBy
. Based on some examples I've seen on the web, I should return the value of some property on the object. Is it right? Why should I get index
as a parameter?
For example, in the following case:
Component.component.ts
constructor() {
window.setInterval(() => this.users = [
{ name: 'user1', score: Math.random() },
{ name: 'user2', score: Math.random() }
],
1000);
}
userByName(index, user) {
return user.name;
}
Component.template.html
<div *ngFor="let user of users; trackBy:userByName">
{{user.name}} -> {{user.score}}
</div>
The objects shown in this template are still updated despite the name being unchanged. Why?
Upvotes: 184
Views: 328431
Reputation: 1596
The trackBy function is an optimization technique in Angular that allows you to specify a unique identifier for each item in the list. Angular will then use this identifier to track changes and update only the necessary parts of the DOM when the list changes.
Explained with a simple example code provided here Optimizing Angular Performance with trackBy in ngFor
Upvotes: 1
Reputation: 101
The purpose of using the trackBy is to set the identity of elements in an iterable. If Angular sees two elements with the same identity, it will proceed to check the content of the elements and will only repaint/re-render if the content is changed. Without the identity, Angular will rely on the object reference of the elements that usually change even when the content is the same, and thus Angular will repaint/re-render the elements because of different references.
Upvotes: 5
Reputation: 104
<div *ngFor="let user of users; trackBy:userByName">
{{user.name}} -> {{user.score}}
</div>
public function userByName(index: number, user: User): id {
return user?.id;
}
Even though i have seen correct answers as above in this thread, i have got some lint/sonar issues for un-used index parameter. so i think the below code can also achieve the action.
<div *ngFor="let user of users; trackBy:userByName">
{{user.name}} -> {{user.score}}
</div>
public function userByName(user: User): number {
return user?.id;
}
Upvotes: 0
Reputation: 105439
On each ngDoCheck
triggered for the ngForOf
directive, Angular checks what objects have changed. It uses differs for this process and each differ uses the trackBy
function to compare the current object with the new one. The default trackBy
function tracks items by identity:
const identify = (index: number, item: any) => item;
It receives the current item and should return some value. Then the value returned by the function is compared against the value this function returned the last time. If the value changes, the differ reports a change. So if the default function returns object references, it will not match the current item if the object reference has changed. So you can provide your custom trackBy
function that will return something else. For example, some key value of the object. If this key value matches the previous one, then Angular will not detect the change.
The syntax ...trackBy:userByName
is no longer supported. You must now provide a function reference. Here is the basic example:
setInterval( () => {
this.list.length = 0;
this.list.push({name: 'Gustavo'});
this.list.push({name: 'Costa'});
}, 2000);
@Component({
selector: 'my-app',
template: `
<li *ngFor="let item of list; trackBy:identify">{{item.name}}</li>
`
})
export class App {
list:[];
identify(index, item){
return item.name;
}
Although the object reference changes, the DOM is not updated. Here is the plunker. If you're curious how ngFor
works under the hood, read this answer.
Upvotes: 222
Reputation: 686
app.component.html
<button class="btn btn-warning" (click)="loadCourses()">LoadCourses</button>
<ul>
<li *ngFor="let course of courses; trackBy:trackCourse">
{{course.name}}
</li>
</ul>
app.component.ts
loadCourses() {
this.courses = [
{id:1, name:'cour1'},
{id:2, name:'cour2'},
{id:3, name:'cour3'}
]
};
trackCourse(index : number, course: any) {
return course ? course.id : undefined;
};
Reference Code With Mosh You can find in Directives Section
Upvotes: 6
Reputation: 6885
As this topic is still active & finding a clear answer is difficult let me add few examples in addition to @Max's answer:
app.component.ts
array = [
{ "id": 1, "name": "bill" },
{ "id": 2, "name": "bob" },
{ "id": 3, "name": "billy" }
]
foo() {
this.array = [
{ "id": 1, "name": "foo" },
{ "id": 2, "name": "bob" },
{ "id": 3, "name": "billy" }
]
}
identify(index, item) {
return item.id;
}
Let's display the array
into 3 divs using *ngFor
.
app.component.html
Example of *ngFor
without trackBy:
<div *ngFor="let e of array;">
{{e.id}} - {{e.name}}
</div>
<button (click)="foo()">foo</button>
What happens if we click the foo
button ?
→ The 3 divs will be refreshed. Try it yourself, open your console to verify.
Example of *ngFor
with trackBy:
<div *ngFor="let e of array; trackBy: identify">
{{e.id}} - {{e.name}}
</div>
<button (click)="foo()">foo</button>
What happens if we click the foo
button ?
→ Only the first div will be refreshed. Try it yourself, open your console to verify.
And what if we updated the first object instead of the whole object ?
foo() {
this.array[0].name = "foo";
}
→ There is no need to use trackBy
here.
It's especially useful when using a Subscription which often looks like what I schematized with array
. So it would look like:
array = [];
subscription: Subscription;
ngOnInit(): void {
this.subscription = this.fooService.getArray().subscribe(data => {
this.array = data;
});
}
identify(index, item) {
return item.id;
}
From the documentation:
To avoid this expensive operation, you can customize the default tracking algorithm. by supplying the trackBy option to NgForOf. trackBy takes a function that has two arguments: index and item. If trackBy is given, Angular tracks changes by the return value of the function.
Read more here: https://angular.io/api/common/NgForOf
Find my original answer here: https://stackoverflow.com/a/57890227/9753985
Upvotes: 80
Reputation: 758
Here's what I use in my projects to allow tracking by a property of the iterated model without the hassle of writing a function in the component's class :
import { Host, Directive, Input } from "@angular/core";
import { NgForOf } from "@angular/common";
@Directive({
selector: "[ngForTrackByProperty]"
})
export class TrackByPropertyDirective {
private _propertyName: string = "";
public constructor(@Host() private readonly _ngFor: NgForOf<any>) {
this._ngFor.ngForTrackBy = (_: number, item: any) => this._propertyName ? item[this._propertyName] : item;
}
@Input("ngForTrackByProperty")
public set propertyName(value: string | null) {
// We must accept null in case the user code omitted the ": 'somePropName'" part.
this._propertyName = value ?? "";
}
}
Usage :
<some-tag *ngFor="let item of models; trackByProperty: 'yourDiscriminantProp'">
Upvotes: 24
Reputation: 462
else you can use
*ngFor="a of array; index as i;"
and
[attr.data-target]="'#test' + i"
and
name="test{{i}}
Upvotes: -16
Reputation: 3141
This angular NgFor
document will help you. https://angular.io/docs/ts/latest/api/common/index/NgFor-directive.html
<div *ngFor="let user of users; trackBy:user?.name">
{{user.name}} -> {{user.score}}
</div>
Upvotes: -15