Why do we need `ngDoCheck`
This great article If you think ngDoCheck
means your component is being checked — read this article explains the error in depth.
The contents of this answer is based on the angular version 2.x.x. For the most recent version 4.x.x see this post.
There is nothing on the internet on the inner workings of change detection, so I had to spend about a week debugging sources, so this answer will be pretty technical on details.
An angular application is a tree of views (AppView
class that is extended by the Component specific class generated by the compiler). Each view has a change detection mode that lives in cdMode
property. The default value for cdMode
is ChangeDetectorStatus.CheckAlways
, which is cdMode = 2
.
When a change detection cycle runs, each parent view checks whether it should perform change detection on the child view here:
detectChanges(throwOnChange: boolean): void {
const s = _scope_check(this.clazz);
if (this.cdMode === ChangeDetectorStatus.Checked ||
this.cdMode === ChangeDetectorStatus.Errored)
return;
if (this.cdMode === ChangeDetectorStatus.Destroyed) {
this.throwDestroyedError('detectChanges');
}
this.detectChangesInternal(throwOnChange); <---- performs CD on child view
where this
points to the child
view. So if cdMode
is ChangeDetectorStatus.Checked=1
, the change detection is skipped for the immediate child and all its descendants because of this line.
if (this.cdMode === ChangeDetectorStatus.Checked ||
this.cdMode === ChangeDetectorStatus.Errored)
return;
What changeDetection: ChangeDetectionStrategy.OnPush
does is simply sets cdMode
to ChangeDetectorStatus.CheckOnce = 0
, so after the first run of change detection the child view will have its cdMode
set to ChangeDetectorStatus.Checked = 1
because of this code:
if (this.cdMode === ChangeDetectorStatus.CheckOnce)
this.cdMode = ChangeDetectorStatus.Checked;
Which means that the next time a change detection cycle starts there will be no change detection performed for the child view.
There are few options how to run change detection for such view. First is to change child view's cdMode
to ChangeDetectorStatus.CheckOnce
, which can be done using this._changeRef.markForCheck()
in ngDoCheck
lifecycle hook:
constructor(private _changeRef: ChangeDetectorRef) { }
ngDoCheck() {
this._changeRef.markForCheck();
}
This simply changes cdMode
of the current view and its parents to ChangeDetectorStatus.CheckOnce
, so next time the change detection is performed the current view is checked.
Check a full example here in the sources, but here is the gist of it:
constructor(ref: ChangeDetectorRef) {
setInterval(() => {
this.numberOfTicks ++
// the following is required, otherwise the view will not be updated
this.ref.markForCheck();
^^^^^^^^^^^^^^^^^^^^^^^^
}, 1000);
}
The second option is call detectChanges
on the view itself which will run change detection on the current view if cdMode
is not ChangeDetectorStatus.Checked
or ChangeDetectorStatus.Errored
. Since with onPush
angular sets cdMode
to ChangeDetectorStatus.CheckOnce
, angular will run the change detection.
So ngDoCheck
doesn't override the changed detection, it's simply called on every changed detection cycle and it's only job is to set current view cdMode
as checkOnce
, so that during next change detection cycle it's checked for the changes. See this answer for details. If the current view's change detection mode is checkAlways
(set by default if onPush strategy is not used), ngDoCheck
seem to be of no use.
The DoCheck
interface is used to detect changes manually which the angular change detection have overlooked. A use could be when you change the ChangeDetectionStrategy
of your component, but you know that one property of an object will change.
It's more efficient to check for this one change, than to let the changeDetector run through your entire component
let obj = {
iChange = 'hiii'
}
If you use obj.iChange
inside your template, angular will not detect it if this value changes, because the reference of obj
itself doesn't change. You need to implement an ngDoCheck
to check if the value has changed, and call a detectChanges
on your component's changeDetector.
From the angular documentation about DoCheck
While the
ngDoCheck
hook can detect when the hero's name has changed, it has a frightful cost. This hook is called with enormous frequency — after every change detection cycle no matter where the change occurred. It's called over twenty times in this example before the user can do anything.Most of these initial checks are triggered by Angular's first rendering of unrelated data elsewhere on the page. Mere mousing into another input box triggers a call. Relatively few calls reveal actual changes to pertinent data. Clearly our implementation must be very lightweight or the user experience will suffer.
tested example
@Component({
selector: 'test-do-check',
template: `
<div [innerHtml]="obj.changer"></div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TestDoCheckComponent implements DoCheck, OnInit {
public obj: any = {
changer: 1
};
private _oldValue: number = 1;
constructor(private _changeRef: ChangeDetectorRef){}
ngOnInit() {
setInterval(() => {
this.obj.changer += 1;
}, 1000);
}
ngDoCheck() {
if(this._oldValue !== this.obj.changer) {
this._oldValue = this.obj.changer;
//disable this line to see the counter not moving
this._changeRef.detectChanges();
}
}
}
Note:
The default algorithm of angular for change detection looks for differences by comparing input-bound properties value by reference, understand. Cool. 🙌
Limitation of ngOnChanges()
Due to default behavior of angular change detection, ngOnChanges can't detect if someone changes a property of an object or push an item into array 😔. So ngDoCheck comes to recuse.
ngDoCheck() 🤩 wow!
Detect deep changes like a property change in object or item is pushed into array even without reference change. Amazing Right 😊