how to use track by inside ngFor angular 2
Asked Answered
P

5

58

tried every syntax i can guess couldnt make it works !

<!--- THIS WORKS FINE --->
<ion-card *ngFor="#post of posts">
{{post|json}}
</ion-card>

<!--- BLANK PAGE --->
<ion-card *ngFor="#post of posts track by post.id">
{{post|json}}
</ion-card>

<!--- Exception : Cannot read property 'id' of undefined --->
<ion-card *ngFor="#post of posts;trackBy:post.id">
{{post|json}}
</ion-card>

<!--- Exception : Cannot read property 'undefined' of undefined --->
<ion-card *ngFor="#post of posts;trackBy:posts[index].id">
{{post|json}}
</ion-card>

<!--- Blank page no exception raised !  --->
<ion-card *ngFor="#post of posts;#index index;trackBy:posts[index].id">
{{post|json}}
</ion-card>

the only approach that worked for me was

  1. Creating method in controller Class

    identify(index,post:Post){ return post.id }

and

<ion-card *ngFor="#post of posts;trackBy:identify">
</ion-card>

is this is only way ? can i not just specify a property name inline for trackBy ?

Platinous answered 31/3, 2016 at 1:57 Comment(6)
Can you please update me what is the use of trackBy in ngFor? Eric's answer seems to be working fine.Boynton
@Boynton It allows you to pass a function to *ngFor that NgFor forwards to https://angular.io/docs/ts/latest/api/core/IterableDifferFactory-interface.html github.com/angular/angular/blob/master/modules/angular2/src/… (don't know how that diffing is working exactly yet though)Protract
https://angular.io/docs/ts/latest/api/core/IterableDifferFactory-interface.html‌​ not found. Any other link?Boynton
Still I'm not clear with clean usage of trackBy.Boynton
i would be very helpful if anyone here post example with the usage of trackBy in *ngForVampire
okay got it thanks @PlatinousVampire
P
97

As pointed out in @Eric comment, and after lots of reading and playing around, here is how to use trackBy in angular2

  1. the first thing you need to know its not same syntax as angular1, now you need to separate it from the for loop with a ;.

Usage 1: Track by property of object

 // starting v2. 1 this will throw error, you can only use functions in trackBy from now on

<ion-card *ngFor="let post of posts;trackBy:post?.id">
</ion-card> // **DEPRECATED**
---or---
<ion-card *ngFor="let post of posts;trackBy:trackByFn">
</ion-card>

here you ask angular2 to

  1. create a local variable post;
  2. you tell trackBy to wait untill this local variable is ready "you do that by using elvis operator 'the question mark after the variable name', then use its id as tracker.

so

// starting v2. 1 this will throw error, you can only use functions in trackBy from now on

*ngFor="#post of posts;trackBy:post?.id"

is what same as angular's 1

ng-repeat="post in posts track by post.id"

Usage 2: Track using your own Function

@Page({
    template: `
        <ul>
            <li *ngFor="#post of posts;trackBy:identify">
              {{post.data}}
            </li>
        </ul>
    `
})
export class HomeworkAddStudentsPage {
    posts:Array<{id:number,data:string}>;   

    constructor() {
        this.posts = [  {id:1,data:'post with id 1'},
                        {id:2,data:'post with id 2'} ];
    }

    identify(index,item){
      //do what ever logic you need to come up with the unique identifier of your item in loop, I will just return the object id.
      return post.id 
     }

}

trackBy can take a name of callback, and it will call it for us supplying 2 parameters: the index of the loop and the current item.

To achieve the same with Angular 1, I used to do:

<li ng-repeat="post in posts track by identify($index,post)"></li>

app.controller(function($scope){
  $scope.identify = function(index, item) {return item.id};
});
Platinous answered 31/3, 2016 at 10:52 Comment(9)
What if the item is an immatable.js map, and to access the id, you need to do item.get('id')?Killer
I do not think that "trackBy:post?.id" works. In my case, the state of the child element is lost. A separate trackBy function works though (but is definitely not convenient). By the way, the elvis operator itself indicates that something is wrong. If it worked, then you would not have to use it here (unless the collection items can be null).Marijo
I can only second what @JoelRichard said: Specifying a field of the traversed item doesn't work for me either. The official docs also don't mention this shortcut syntax: angular.io/docs/ts/latest/api/common/index/NgFor-directive.html.Kreager
@Platinous It seems the syntax is outdated and should be <li *ngFor="let item of items; let i = index; trackBy: trackByFn">...</li> can you update it, please?Centaur
@Birowsky for immutables u have to use functions and return item.get('id')Platinous
I confirm, trackBy only works with functions, it doesn't error with "post?.id" but it will trackBy "null" in this case (you can inspect the html and see that), which means that it doesn't track at all, you just don't see an error @Platinous could you update your post so that it is not misleading for new users ?Peremptory
In addition to @Peremptory comment. Starting with Angular 2.4.1, using *ngFor="#post of posts;trackBy:post?.id" will thrown an error, because trackBy only accepts functionsAcred
Right @Zalaboza, only function is allowed, please update your post. Discussed here: github.com/angular/angular/issues/13641#issuecomment-269082370Gatekeeper
@Platinous Would you please update your answer which trackBy:post?.id has been confirmed not working.Liston
A
11

As you already recognized, using a function is the only way to use trackBy in Angular 2

<ion-card *ngFor="#post of posts;trackBy:identify"></ion-card>

The official documentation states that https://angular.io/docs/ts/latest/api/common/index/NgFor-directive.html

All the other information about <ion-card *ngFor="let post of posts;trackBy:post?.id"></ion-card> is wrong. Starting with Angular 2.4.1 this will also throw an error in the application.

Acred answered 22/12, 2016 at 12:56 Comment(0)
L
6

Just want to add few examples (Angular 2+) in addition to others' answer to make the use of trackBy clear.

From 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

An example will explain it better.

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 div 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 happend if we click on 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 happend if we click on 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 usefull when using Subscription which often looks like what I schematized with array. So it would looks like:

   array = [];
   subscription: Subscription;

   ngOnInit(): void {
      this.subscription = this.fooService.getArray().subscribe(data => {
         this.array = data;
      });
   }

   identify(index, item) {
      return item.id;
   }
Labret answered 11/9, 2019 at 13:22 Comment(0)
F
1

The concept behind trackBy:

  1. ngFor of angular automatically optimizes the display of modified/created/deleted objects by tracking through object identity. So, if you create all new objects in the list and then use ngFor, it will render whole list.

  2. Let's consider a scenario where despite of all ngFor optimizations, the rendering is still taking time. In that case we use trackBy. So that, we can provide another parameter to track objects than the object identity which is a default tracking criteria.

A running example:

<!DOCTYPE html>
<html>

<head>
    <title>Angular 2.1.2 + TypeScript Starter Kit</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <script src="https://unpkg.com/[email protected]/dist/zone.js"></script>
    <script src="https://unpkg.com/[email protected]/Reflect.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/system.js"></script>
    <script src="https://unpkg.com/[email protected]/lib/typescript.js"></script>
    <script src="config.js"></script>
  <script>
      System.import('app')
          .catch(console.error.bind(console));
    </script>
</head>

<body>
    <my-app>
        loading...
    </my-app>
</body>

</html>
Foundling answered 19/5, 2017 at 8:37 Comment(3)
Not sure why you have that 'running example' which neither runs nor has an example of trackby, but +1 for the reasons behind trackby. It is only for performance improvement.Racecourse
@Racecourse Looks a lot like the non-example-example linked to from this blog post on trackBy ;^)Fullgrown
Why do you show that code? Also, the question is about trackBy usage, not why it is used. The official documentation and dozens of blog posts out there explain why trackBy is important.Geter
I
1

This is Angular global trackBy property directive with strict type checking, for handle trackBy entirely in the template by passing a property:

import { NgForOf } from '@angular/common';
import { Directive, Host, Input, NgIterable } from '@angular/core';

@Directive({
    selector: '[ngForTrackByProperty]'
})
export class NgForTrackByPropertyDirective<T> {

    @Input() ngForOf!: NgIterable<T>;
    @Input() ngForTrackByProperty!: keyof T;

    constructor(@Host() ngForOfDir: NgForOf<T>) {
        ngForOfDir.ngForTrackBy = (_, item: T): T[keyof T] => item[this.ngForTrackByProperty];
    }
}

usage

import { Component } from '@angular/core';

interface Item { 
  id: number; 
  name: string;
}

@Component({
  selector: 'app-root',
  template: `
    <ul>
      <li *ngFor="let item of list; trackByProperty: 'id'">
        {{ item.id }} {{ item.name }}
      </li>
    </ul>
  `,
})
export class AppListComponent {
  public list: Array<Item> = [
    { id: 0, name: 'foo' },
    { id: 1, name: 'bar' },
    { id: 2, name: 'baz' },
  ];
}

NPM https://www.npmjs.com/package/ng-for-track-by-property

github https://github.com/nigrosimone/ng-for-track-by-property

Induce answered 12/3, 2022 at 11:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.