Reputation: 152
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.
Upvotes: 3
Views: 11733
Reputation: 155
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 component, 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 component. 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.com/edit/angular-vcebqu and make ff changes.
on file
parent.component.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.
Upvotes: 3
Reputation: 66
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 };
}
}
}
Upvotes: 3
Reputation: 54771
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:
OnPush
only sets inputs when they have changed.this.cd.markForCheck()
in a setter because the component is already dirty.Upvotes: 0