Updating value in parent component from child one causes ExpressionChangedAfterItHasBeenCheckedError in Angular

笑着哭i 提交于 2019-11-28 11:14:32

The article Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error explains this behavior in great details.

There are two possible solutions to your problem:

1) put app-child before ngIf:

<app-child></app-child>
<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>

2) Use asynchronous event:

export class TitleService {
  private titleChangeEmitter = new EventEmitter<string>(true);
                                                       ^^^^^^--------------

After thorough investigation the problem only occurred when using *ngIf="title"

The problem you're describing is not specific to ngIf and can be easily reproduced by implementing a custom directive that depends on parent input that is synchronously updated during change detection after that input was passed down to a directive:

@Directive({
  selector: '[aDir]'
})
export class ADirective {
  @Input() aDir;

------------

<div [aDir]="title"></div>
<app-child></app-child> <----------- will throw ExpressionChangedAfterItHasBeenCheckedError

Why that happens actually requires a good understanding of Angular internals specific to change detection and component/directive representation. You can start with these articles:

Although it's not possible to explain everything in details in this answer, here is the high level explanation. During digest cycle Angular performs certain operations on child directives. One of such operations is updating inputs and calling ngOnInit lifecycle hook on child directives/components. What's important is that these operations are performed in strict order:

  1. Update inputs
  2. Call ngOnInit

Now you have the following hierarchy:

parent-component
    ng-if
    child-component

And Angular follows this hierarchy when performing above operations. So, assume currently Angular checks parent-component:

  1. Update title input binding on ng-if, set it to initial value undefined
  2. Call ngOnInit for ng-if.
  3. No update is required for child-component
  4. Call ngOnInti for child-component which changes title to Dynamic Title on parent-component

So, we end up with a situation where Angular passed down title=undefined when updating properties on ng-if but when change detection is finished we have title=Dynamic Title on parent-component. Now, Angular runs second digest to verify there's no changes. But when it compares to what was passed down to ng-if on the previous digest with the current value it detects a change and throws an error.

Changing the order of ng-if and a child-component in the parent-component template will lead to the situation when property on parent-component will be updated before angular updates properties for a ng-if.

You could try using the ChangeDetectorRef so Angular will be manually notified about the change and the error won't be thrown.
After changing title in ParentComponent call the detectChanges method of the ChangeDetectorRef.

export class ParentComponent implements OnInit, OnDestroy {

  title: string;
  private titleSubscription: Subscription;

  constructor(private titleService: TitleService, private changeDetector: ChangeDetectorRef) {
  }

  public ngOnInit() {
    this.titleSubscription = this.titleService.onTitleChange()
      .subscribe(title => this.title = title);

    this.changeDetector.detectChanges();
  }

  public ngOnDestroy(): void {
    if (this.titleSubscription
    ) {
      this.titleSubscription.unsubscribe();
    }
  }

}

In my case, I was changing the state of my data--this answer requires you to read AngularInDepth.com explanation of digest cycle--inside the html level, all I had to do was change the way I was handling the data like so:

<div>{{event.subjects.pop()}}</div>

into

<div>{{event.subjects[0]}}</div>

Summary: instead of popping--which returns and remove the last element of an array thus changing the state of my data-- I used the normal way of getting my data without changing its state thus preventing the exception.

A 'quick' solution in some cases is to use setTimeout(). You need to consider all the caveats (see the articles other people have referred to) but for a simple case like just setting a title this is the easiest way.

ngOnInit (): void {

  // Watching for title change.
  this.titleSubscription = this.titleService.onTitleChange().subscribe(title => {

    setTimeout(() => { this.title = title; }, 0);

 });
}

Use this as a last resort type solution if you're not yet comfortable understanding all the complexities of change detection. Then come back and fix it another way when you are :)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!