After reading your input and feeling curious, I delved deep into the angular differ code.
I have dissected the behavior in three different scenarios for you and believe it's valuable knowledge to possess:
First scenario:
If no trackBy
is defined:
<div *ngFor="let obj of arrayOfObj">{{obj.key}}</div>
In this case, when a trackBy function isn't specified, Angular loops through the array, creates the DOM elements, and binds the data within the template using [ngForOf]
. (The above code can be rendered as):
<ng-template ngFor let-obj [ngForOf]="arrayOfObj">
<div>{{obj.key}}</div>
</ng-template>
Initially, it generates all those div elements which is consistent across all three possibilities. However, when new data arrives from the API with similar but not identical objects, Angular compares them based on identity using ===
. This method works well for strings, numbers, and other primitives, but falls short for objects. Subsequently, when Angular checks for changes, it cannot locate the original objects in order to update them. As a result, the DOM elements are removed and rebuilt entirely.
Even if the data remains unchanged, different identities of objects in the second response prompt Angular to recreate the entire DOM structure (as if old elements were deleted and new ones inserted).
This process can be resource-intensive both in terms of CPU usage and memory consumption.
Second scenario:
trackBy
defined with object key:
<div *ngFor="let obj of arrayOfObj;trackBy:trackByKey">{{obj.key}}</div>
trackByKey = (index: number, obj: object): string => {
return object.key;
};
This approach is the most efficient one. When new data containing objects with different identities than before is received, Angular utilizes the trackBy function to determine the object's identity. It then matches it against existing (and previously deleted if not found) DOM elements. If a match is found, it updates the bindings inside the template. Otherwise, it searches through the removed objects and creates a new DOM element if necessary. Thus, this process is swift as it involves updating existing DOM elements and their bindings.
Third scenario:
trackBy
defined with array index
<div *ngFor="let obj of arrayOfObj;trackBy:trackByIndex">{{obj.key}}</div>
trackByIndex = (index: number): number => {
return index;
};
Similar to tracking by object key, this method keeps updating the bindings within templates even when items are repositioned within the array. While still efficient, it may not be the quickest method available, although it certainly beats reconstructing the entire DOM structure.
Hopefully, you now grasp the distinctions between these approaches. As an added tip, if your business objects share a common way to access their identity (such as a property like .id
or .key
), you can extend the native *ngFor
and create a custom structural directive with a built-in trackBy
function. Here's a snippet that has not been tested:
export interface BaseBo {
key: string;
}
@Directive({selector: '[boFor][boForOf]'})
export class ForOfDirective<T extends BaseBo> extends NgForOf<T> {
@Input()
set boForOf(boForOf: T[]) {
this.ngForOf = boForOf;
}
ngForTrackBy = (index: number, obj: T) => obj.key;
}