简体   繁体   中英

Angular OnPush Change Detection Propagation to a Child Components in a ngFor Loop

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.

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 method onClick($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.

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 };
    }
  }
}

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 component is already dirty.
  • You don't have to output the value from the input. The parent component already knows what was inputted.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM