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.com/edit/angular-vcebqu
The application contains a parent
component and a child
component.
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
component creates several child
components 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
component, the changes are not being propogated down the the child
component(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
component's 'element'. However, if OnPush change detection is removed from the child
component, it works properly.
Since the input passed in to the child component 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.component.html',
styleUrls: ['./child.component.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 };
}
}
}