I am having an issue with onPush Change Detection in an Angular app.
I have created a demo app that illustrates the problem:
The application contains a parent
ponent and a child
ponent.
Both parent
and child
are using onPush Change Detection.
Both parent
and child
have inputs broken out into getters and setters, with this.cd.markForCheck();
being used in the setters.
private _element: any;
@Output()
elementChange = new EventEmitter<any>();
@Input()
get element() {
return this._element;
}
set element(newVal: any) {
if (this._element === newVal) { return; }
this._element = newVal;
this.cd.markForCheck();
this.elementChange.emit(this._element);
}
The parent
ponent creates several child
ponents using a *ngFor
loop, like so:
<app-child
*ngFor="let element of item.elements; let index = index; trackBy: trackElementBy"
[element]="item.elements[index]"
(elementChange)="item.elements[index]=$event"></app-child>
The problem is, if the data is updated in the parent
ponent, the changes are not being propogated down the the child
ponent(s).
In the demo app, click the 'change' button and notice that the first 'element' in the 'elements' array ( elements[0].order
) is updated in the parent, but the change does not show in the the first child
ponent's 'element'. However, if OnPush change detection is removed from the child
ponent, it works properly.
I am having an issue with onPush Change Detection in an Angular app.
I have created a demo app that illustrates the problem: https://stackblitz./edit/angular-vcebqu
The application contains a parent
ponent and a child
ponent.
Both parent
and child
are using onPush Change Detection.
Both parent
and child
have inputs broken out into getters and setters, with this.cd.markForCheck();
being used in the setters.
private _element: any;
@Output()
elementChange = new EventEmitter<any>();
@Input()
get element() {
return this._element;
}
set element(newVal: any) {
if (this._element === newVal) { return; }
this._element = newVal;
this.cd.markForCheck();
this.elementChange.emit(this._element);
}
The parent
ponent creates several child
ponents using a *ngFor
loop, like so:
<app-child
*ngFor="let element of item.elements; let index = index; trackBy: trackElementBy"
[element]="item.elements[index]"
(elementChange)="item.elements[index]=$event"></app-child>
The problem is, if the data is updated in the parent
ponent, the changes are not being propogated down the the child
ponent(s).
In the demo app, click the 'change' button and notice that the first 'element' in the 'elements' array ( elements[0].order
) is updated in the parent, but the change does not show in the the first child
ponent's 'element'. However, if OnPush change detection is removed from the child
ponent, it works properly.
3 Answers
Reset to default 3Since the input passed in to the child ponent isn't an Array, IterableDiffers won't work. KeyValueDiffers however can be used in this case to watch for changes in the input object and then handle it accordingly (stackblitz link):
import {
Component,
OnInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
KeyValueDiffers,
KeyValueDiffer,
EventEmitter,
Output, Input
} from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.ponent.html',
styleUrls: ['./child.ponent.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {
private _element: any;
@Output()
elementChange = new EventEmitter<any>();
get element() {
return this._element;
}
@Input()
set element(newVal: any) {
if (this._element === newVal) { return; }
this._element = newVal;
this.cd.markForCheck();
this.elementChange.emit(this._element);
}
private elementDiffer: KeyValueDiffer<string, any>;
constructor(
private cd: ChangeDetectorRef,
private differs: KeyValueDiffers
) {
this.elementDiffer = differs.find({}).create();
}
ngOnInit() {
}
ngOnChanges() {
// or here
}
ngDoCheck() {
const changes = this.elementDiffer.diff(this.element);
if (changes) {
this.element = { ...this.element };
}
}
}
Just adding an alternative solution just in case someone else gets this problem. The main reason why ChildComponent doesn't reflect the new value in its template is because only the property 'order' of 'element' is getting changed from the parent ponent, so the parent is injecting the same object reference with a modified 'order' property.
OnPush change detection strategy will only 'detect changes' when a new object reference is injected into the ponent. So in order for ChildComponent (which has OnPush change detection strategy) to trigger change detection, you have to inject a new object reference to the "element" input property instead of the same.
To see this in action, open https://stackblitz./edit/angular-vcebqu and make ff changes.
on file
parent.ponent.ts
, modify the methodonClick($event) {...}
to:
onClick(event){
const random = Math.floor(Math.random() * (10 - 1 + 1)) + 1;
this.item.elements[0] = {...this.item.elements[0], order: random};
}
The last line replaces the object reference inside the array at index 0 with a new object identical to old first element in the array, except for the order property.
You have to add the @Input()
decorator to the setter method.
get element() {
return this._element;
}
@Input()
set element(newVal: any) {
this._element = newVal;
}
Also here are some other things:
- Angular won't set duplicate values because
OnPush
only sets inputs when they have changed. - Do not call
this.cd.markForCheck()
in a setter because the ponent is already dirty. - You don't have to output the value from the input. The parent ponent already knows what was inputted.