How to use `trackBy` with `ngFor`
Asked Answered
T

8

178

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?

Triadelphous answered 8/2, 2017 at 8:35 Comment(5)
have you tried to write let user of users; let index=index; trackBy:userByName(index,user) ?Perlite
you can return index which will be unique for each item.Ladle
@micronyks, I don't understand. Can you elaborate? With an example maybe?Triadelphous
What you don't understand? could you please tell me. I told you to return index that's it.Ladle
I don't understand how the comparing mechanism of ngFor is working where ngTrackBy come inTriadelphous
T
217

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.

Triadelphous answered 29/7, 2017 at 15:58 Comment(7)
Can I use same trackByFn for 2 loops?Quinidine
@YashwardhanPauranik, if the function is pure, i.e. simply returns the result based on inputs, I don't see why you can'tTriadelphous
I cannot acess this (compoennt iteself) in trackbyfn . any way to do that?Patrizius
@MaximKoretskyi And the answer to your question "The objects shown in template are still updated despite the name being unchanged. Why?" actually has nothing to do with trackBy, but rather with Angular change detection, right? I mean, the <div> itself won't get repainted (removed and then added again), but the content of the <div> will get updated anyway because in the template you bind user.score that changes every interval tick.Inurn
@GlennMohammad 's statement makes sense. TrackBy only answers the question "whether to delete and recreate the DOM element? ". The {{content}} inside template tag will be rendered through for loop **regardless of the value returned by trackBy(**as long as DOM exists). ----- Mostly efficiency prevails when trackBy tells angular to render the changes without destroying the DOM. ----- AFAIU comparison include index too; like " ( index, tackBy(index, item) ) ". ----- ie: what was the return value by trackBy for current index lastTime.Horace
Compare "trackBy returns a constant value" vs "trackBy returns an ID from Object item". trackByConstant ideally say don't destroy & recreate the DOM even if data list is re-initialize. trackByID say only do if ID changes. But if we swap the data, btw distant index ie;3 & 6. You will see DOM destroy & recreation for both, may be bcz it take index too. trackByConstant work as normal swap the DOM in position. But trackByID seems to touch all intermediate DOM (3,4,5,6). The one above 3 and Below 6 remain untouched. Its just an observation, its helpful if someone could clarify this.Horace
stackblitz.com/edit/angular-ivy-jd9jtf?file=src/app/my-list/… It may not be destroyed but if you inspect the <li></li> elements under trackByConstant and trackBYID, you will see the glowing effect for DOM(3,4,5,6) indeces.Horace
A
78

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://mcmap.net/q/144178/-how-to-use-track-by-inside-ngfor-angular-2

Anchorite answered 20/9, 2019 at 9:46 Comment(7)
I don't really understand, why only the first object gets updated in the second case (using trackBy). The identify method, which is used for detecting changes returns the item.id, which stays 1 before and after pressing the button. Only the name changed from bill to foo, but because the name is not compared, the method should not detect any change. I expected that nothing would have been updated in this case. I would have used item.name, but this seems to be the wrong approach. What am I missing here? Can someone clarify this please?Doley
@Doley identify is used to identify objects from your *ngFor array → you have to choose a static value like an id. Have a look here angular.io/api/common/NgForOf#ngForTrackBy if you want to learn more about ngForTrackBy.Anchorite
I logged the index and item inside the trackBy function and it is logging nonstop. Is this normal?Threephase
@PrajilShrestha yes this is the normal behavior, it has nothing to do with trackBy, it's about Angular cycle (angular.io/guide/lifecycle-hooks) and its change detection (angular.io/api/core/ChangeDetectorRef).Anchorite
very good explanation thanksVanhouten
The Angular docs for TrackByFunction (angular.io/api/core/TrackByFunction) specifically state, "NgForOf needs to uniquely identify items in the iterable to correctly perform DOM updates when items in the iterable are reordered, new items are added, or existing items are removed." The example above exhibits none of these. It goes on to say, "In all of these scenarios it is usually desirable to only update the DOM elements associated with the items affected by the change."Anticyclone
@Doley It's becouse trackBy tracks if an element should be re-rendered, which in this case it does not, BUT the change detection still updates (not re-renders) the value of element. So the trackBy is actually working properly.Vitrine
F
22

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'">

Foiled answered 31/7, 2021 at 0:23 Comment(0)
A
5

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

Abbyabbye answered 31/3, 2022 at 11:11 Comment(0)
M
5

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.

Macaronic answered 30/6, 2022 at 17:21 Comment(0)
A
0
<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;
}
Aton answered 28/7, 2023 at 10:33 Comment(0)
C
-15

This angular NgFor document will help you. https://angular.io/docs/ts/latest/api/common/index/NgFor-directive.html

Below example for your code

<div *ngFor="let user of users; trackBy:user?.name">
 {{user.name}} -> {{user.score}}
</div>
Constringent answered 8/2, 2017 at 9:21 Comment(2)
I can't seem to find an example of trackBy used not with callback function, but by passing iterated item's property directly. Can you confirm this usage somehow?Socalled
You cannot track by null, everyone should stop using the "?" blindly. So much bad code exists because of it.Subminiature
S
-16

else you can use

*ngFor="a of array; index as i;"

and

[attr.data-target]="'#test' + i"

and

name="test{{i}}
Seligman answered 6/12, 2019 at 5:40 Comment(1)
These are not trackBy. Please see angular.io/api/common/NgForOf#ngForTrackBy.Dysphagia

© 2022 - 2024 — McMap. All rights reserved.