Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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>
like image 754
Slava Fomin II Avatar asked Jun 20 '17 14:06

Slava Fomin II


3 Answers

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:

  • The mechanics of DOM updates in Angular
  • Everything you need to know about change detection in Angular
  • Angular’s $digest is reborn in the newer version of Angular

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.

like image 171
Max Koretskyi Avatar answered Oct 21 '22 12:10

Max Koretskyi


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

}
like image 33
Unsinkable Sam Avatar answered Oct 21 '22 13:10

Unsinkable Sam


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 :)

like image 28
Simon_Weaver Avatar answered Oct 21 '22 12:10

Simon_Weaver