Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
448 views
in Technique[技术] by (71.8m points)

angular2 changedetection - Updating value in parent component from child one causes ExpressionChangedAfterItHasBeenCheckedError in Angular

I have two component: ParentComponent > ChildComponent and a service, e.g. TitleService.

ParentComponent looks like this:

export class ParentComponent implements OnInit, OnDestroy {

  title: string;


  private titleSubscription: Subscription;


  constructor (private titleService: TitleService) {
  }


  ngOnInit (): void {

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

  }

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

}

ChildComponent looks like this:

export class ChildComponent implements OnInit {

  constructor (
    private route: ActivatedRoute,
    private titleService: TitleService
  ) {
  }


  ngOnInit (): void {

    // Updating title.
    this.titleService.setTitle(this.route.snapshot.data.title);

  }

}

The idea is very simple: ParentController displays the title on screen. In order to always render the actual title it subscribes to the TitleService and listens for events. When title is changed, the event happens and title is updated.

When ChildComponent loads, it gets data from the router (which is resolved dynamically) and tells TitleService to update the title with the new value.

The problem is this solution causes this error:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'Dynamic Title'.

It looks like the value is updated in a change detection round.

Do I need to re-arrange the code to have a better implementation or do I have to initiate another change detection round somewhere?

  • I can move the setTitle() and onTitleChange() calls to the respected constructors, but I've read that it's considered a bad practice to do any "heavy-lifting" in the constructor logic, besides initializing local properties.

  • Also, the title should be determined by the child component, so this logic couldn't be extracted from it.


Update

I've implemented a minimal example to better demonstrate the issue. You can find it in the GitHub repository.

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

<p>Parent Component</p>

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

<hr>

<app-child></app-child>
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...