XXIV
XXIV

Reputation: 358

Angular trackby function using undefined side effects

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

Answers (2)

Alberto Chiesa
Alberto Chiesa

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 ngFors 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

Kurt Hamilton
Kurt Hamilton

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.

The setup

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

  1. create list
  2. partially replace some list data
  3. sort list
  4. reset list data

I assigned new instances of objects at each step for a "pure" test.

The results

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.

Conclusions

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

Related Questions