When we use
ngDoCheck
to detect changes, we need to make sure that our implementation is extremely lightweight and fast, so it doesn’t affect user-experience. In this tutorial, we will learn how to efficiently track and process those changes usingKeyValueDiffer
.
ngDoCheck
life-cycle hook
The official definition of this life-cycle hook goes like this:
"Detect and act upon changes that Angular can't or won't detect on its own. Called immediately after ngOnChanges() on every change detection run, and immediately after ngOnInit() on the first run."
Simply put, Angular tracks binding inputs by object reference. It means that if an object reference hasn’t changed, the binding change is not detected and change detection is not executed. This is where we need ngDoCheck
.
Practical usage
It is very important to understand when to use ngDoCheck
life-cycle hook when working with the code and how it’s different from ngOnChanges
.
For example, we are going to consider two components:
-
my-app
- Has the basic layout andrates
property, which represents the rates of INR for 1 USD over time. -
app-rates
- Accepts single@Input
forrates
Our goal is to track changes of rates.inr
and display the same in app-rates
. Let’s start with coding:
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<button (click)="updateRates()">Update rates</button>
<div>
<h4>{{ 1 | currency }} = {{ rates.inr | currency: 'INR' }}</h4>
<app-rates [rates]="rates"></app-rates>
</div>
`,
})
export class AppComponent {
rates: { inr: number } = { inr: 0 };
updateRates() {
this.rates.inr = 75;
}
}
my-app
’s code is basic. It just displays the rates
and we have also given a button
which will update the rates
by calling updateRates
.
Let’s look at app-rates
’s code:
// rates.component.ts
import {
Component,
DoCheck,
Input,
OnChanges,
SimpleChanges,
} from '@angular/core';
@Component({
selector: 'app-rates',
template: `
<span
*ngIf="diff !== undefined; else noDiff"
class="badge"
[class.bg-success]="diff > 0"
[class.bg-danger]="diff < 0"
>
{{ diff | number: '1.0-2' }}
</span>
<ng-template #noDiff>
<span class="badge bg-secondary">
No difference
</span>
</ng-template>
`,
})
export class RatesComponent {
@Input() rates: { inr: number } = { inr: 0 };
diff = undefined;
}
app-rates
’s template only displays diff
, which represents how much rates.inr
has changed since last time. And if there is no change, it will show “No difference” text.
Now, to simply get diff
, we will need to calculate the difference between new value and old value.
Why not ngOnChanges
We may think to do this with ngOnChanges
. Let’s first see what changes we are getting in ngOnChanges
life-cycle hook:
export class RatesComponent implements OnChanges {
// ...
ngOnChanges(changes: SimpleChanges) {
console.log('Is first change?', changes.rates.firstChange);
}
}
Now, let’s keep an eye on the console and click on the “Update rates” button:
Notice that ngOnChanges
is getting called only when the rates
is assigned for the first time. This is happening because we are not changing the rates
object by reference from my-app
. If we write something like below in my-app
, then ngOnChanges
will capture the changes:
updateRatesByReference() {
this.rates = { ...this.rates, inr: 70 };
}
Usage of ngDoCheck
Unlike ngOnChanges
, ngDoCheck
tracks all the changes, whether they are by reference or not and even more. Let’s utilise it in our example:
export class RatesComponent implements DoCheck {
@Input() rates: { inr: number } = { inr: 0 };
diff = undefined;
oldRate = 0;
ngDoCheck() {
if (this.rates.inr !== this.oldRate) {
this.diff = this.rates.inr - this.oldRate;
this.oldRate = this.rates.inr;
}
}
}
In the above code, we introduced a new property called oldRate
. And in ngDoCheck
we are checking if the new rates.inr
is not same as oldRate
, then it should update the diff
. Let’s look at the output now:
For more on ngDoCheck
, I would recommend you to read the article: If you think ngDoCheck
means your component is being checked — read this article - Angular inDepth.
This example is available on stackblitz. This code gives the result as expected. But Angular provides few utilities to efficiently track changes made to an object over time. Let’s look into those.
KeyValueDiffer and utilities
There are a few interfaces and a service involved when we want to use KeyValueDiffer
. Below is the illustration which covers them all:
Below is the summary:
- We will inject the service
KeyValueDiffers
and use itsfind()
method to get aKeyValueDifferFactory
- Next, we will use
KeyValueDifferFactory
’screate()
method to createKeyValueDiffer
- We will track the changes through the
KeyValueDiffer
’sdiff()
method. It returnsKeyValueChanges
- And at last, we will analyse the changes from
KeyValueChanges
using one of its methods, for exampleforEachChangedItem
- All methods provide access to change-record
KeyValueChangeRecord
- The
KeyValueChangeRecord
interface is a record representing the item change information
- All methods provide access to change-record
Practical usage
We will use the above utilities in the app-rates
which we created previously. We will start with blank ngDoCheck
:
export class RatesComponent implements DoCheck {
@Input() rates: { inr: number } = { inr: 0 };
diff = undefined;
ngDoCheck() {}
}
Our goal here is to track the changes made to rates
property with KeyValueDiffer
utilities.
Property of type KeyValueDiffer
Let’s first create a differ
:
differ: KeyValueDiffer<string, number>;
As the rates
object has the key of type string
and value of type number
, we are passing two types, string
and number
respectively with KeyValueDiffer
. You can change this as per your need.
Inject KeyValueDiffers
service
Next, let’s inject the KeyValueDiffers
service:
constructor(private _differsService: KeyValueDiffers) {}
Initialize KeyValueDiffer
It’s time to initialize the differ
from service. We will do it in ngOnInit
life-cycle hook:
ngOnInit() {
this.differ = this._differsService.find(this.rates).create();
}
In the above code, first we are calling the find()
method. This method internally first checks if the object passed as argument is either a Map
or JSON and if the check is successful then it returns KeyValueDiffersFactory
. You can checkout it’s source-code on GitHub, but overall, below is how it looks:
find(kv: any): KeyValueDifferFactory {
const factory = this.factories.find(f => f.supports(kv));
if (factory) {
return factory;
}
throw new Error(`Cannot find a differ supporting object '${kv}'`);
}
After find()
, we are calling the create()
method of KeyValueDiffersFactory
, which creates a KeyValueDiffer
object.
Track changes in ngDoCheck
Next, we will use the differ
and call it’s diff()
method inside ngDoCheck
:
ngDoCheck() {
if (this.differ) {
const changes = this.differ.diff(this.rates);
}
}
The diff()
method returns KeyValueChanges
or null
. As mentioned earlier KeyValueChanges
provides methods to track all the changes, additions, and removals.
In our case, we need to track changes made to rates
, so we will use forEachChangedItem()
and calculate the diff
:
ngDoCheck() {
if (this.differ) {
const changes = this.differ.diff(this.rates);
if (changes) {
changes.forEachChangedItem((r) => {
this.diff = r.currentValue.valueOf() - r.previousValue.valueOf();
});
}
}
}
The final code of app-rates
looks like below:
@Component({
selector: 'app-rates',
template: `
<span
*ngIf="diff !== undefined; else noDiff"
class="badge"
[class.bg-success]="diff > 0"
[class.bg-danger]="diff < 0"
>
{{ diff | number: '1.0-2' }}
</span>
<ng-template #noDiff>
<span class="badge bg-secondary">
No difference
</span>
</ng-template>
`,
})
export class RatesComponent implements DoCheck, OnInit {
@Input() rates: { inr: number } = { inr: 0 };
oldRate = 0;
diff = undefined;
differ: KeyValueDiffer<string, number>;
constructor(private _differsService: KeyValueDiffers) {}
ngOnInit() {
this.differ = this._differsService.find(this.rates).create();
}
ngDoCheck() {
if (this.differ) {
const changes = this.differ.diff(this.rates);
if (changes) {
changes.forEachChangedItem((r) => {
this.diff = r.currentValue.valueOf() - r.previousValue.valueOf();
});
}
}
}
}
This example is also available on stackblitz.
Conclusion
We first started with a brief intro to ngDoCheck
. Then we learned the utilities needed to track the changes, i.e. interfaces KeyValueDiffer
, KeyValueChanges
, KeyValueChangeRecord
and KeyValueDifferFactory
and KeyValueDiffers
service.
Finally, we implemented it all in the code and tracked the changes made to the rates
object over time using KeyValueChanges.forEachChangedItem
.
This strategy is also used by Angular’s built-in directive ngStyle
, you can check it’s code on GitHub.
In this tutorial, we learned about tracking changes made to an object. It is also possible to track changes made to an array. For that, you will need to use IterableDiffers
service and related interfaces in the same manner. For more on it, checkout ngClass
’s code on GitHub, where the Angular team have used IterableDiffers
.
Top comments (0)