Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating boolean in AfterViewInit causes "Expression has changed after it was checked"

I have a simple alert component which I'm creating dynamically in the view. Since it's created dynamically I've set an option to automatically display the alert after it has been initialised.

Although it's working I'd like to understand why I have to manually trigger the change detection in this particular case.

Code:

export class OverlayMessageComponent implements AfterViewInit {
    ...

    ngAfterViewInit() {
        if(this.autoShow) {
            this.show();
        }
        this.changeDetector.detectChanges();
    }

    ...
}

Full Sample: https://plnkr.co/edit/8NvfhDvLVBd71I7DR0kW

I had to add this.changeDetector.detectChanges(); as I was getting the following error:

EXCEPTION: Expression has changed after it was checked.

I was under the impression that using AfterViewInit helps avoiding the issue, but I think I'm assuming wrong. Is there a way to structure the code better to avoid this error?

I'd like to understand better why this error is returned. I've seen this error a few times before, and I know that some say that with a setTimeout() or enableProdMode() does solve the issue, but to me it seems a hacky workaround when the framework itself is notifying you that there's a problem.

like image 992
Daniel Grima Avatar asked Jul 10 '17 12:07

Daniel Grima


People also ask

How do you fix expression has changed after it was checked?

Navigate up the call stack until you find a template expression where the value displayed in the error has changed. Ensure that there are no changes to the bindings in the template after change detection is run. This often means refactoring to use the correct component lifecycle hook for your use case.

What is the difference between ngAfterContentInit and ngAfterViewInit?

ngAfterContentInit : This is called after components external content has been initialized. ngAfterViewInit : This is called after the component view and its child views has been initialized.

What is ngAfterViewInit?

ngAfterViewInit()linkA callback method that is invoked immediately after Angular has completed initialization of a component's view. It is invoked only once when the view is instantiated.

What is change detection strategy in Angular?

What is Change Detection Strategy in Angular ? Angular Change Detection Strategy are the methods by which the updates to the component is tracked and component is triggered to Re-render.


2 Answers

I'd like to understand better why this error is returned

The AfterViewInit and AfterViewChecked lifecycle hooks are triggered after change detection has completed and the view has been built. So any code that runs at this point should not update the view, or your app and its view will fall out of sync. Take a look at the docs

Angular's unidirectional data flow rule forbids updates to the view after it has been composed. Both of these hooks fire after the component's view has been composed.

Angular throws an error if the hook updates the component's data-bound comment property immediately.

As a result, you must either manually trigger change detection -- which is an expensive operation because Angular has to go through the whole App again -- or make the change asynchronously so that the view will be updated at the next change detection step, with something such as:

if(this.autoShow) { setTimeout(()=>this.show,0)}

Or more simply, if you don't need to grab a handle of something in the view, you can run your code in ngOnInit() or a little later in ngAfterContentInit(). Because these run before the view is composed, you can make changes that affect the view without trouble.

Docs: lifecycle hooks order

like image 135
BeetleJuice Avatar answered Oct 15 '22 12:10

BeetleJuice


The fix

For your particular case there's no need to trigger change detection or use async update. The fix is simple, just move the this.show to the ngOnInit lifecycle hook:

  ngOnInit() {
    if(this.autoShow) {
      this.show();
    }
  }

The explanatation

Because you're using bringIconToFront component property in the template binding:

<div class="icon home" [class.add-z-index]="bringIconToFront"></div>

Angular should update the DOM of the App component. Also, Angular calls lifecycle hooks for the child OverlayMessage component. DOM udpate and lifecycle hooks are performed in order as shown here:

  • calls OnInit and ngDoCheck on a child component (OnInit is called only during first check)
  • updates DOM interpolations and bindings for the current App view if properties on current view component instance changed`
  • calls ngAfterViewInit and ngAfterViewChecked for the child OverlayMessage component
  • calls ngAfterViewInit and ngAfterViewChecked for the current App component

You can see that the onInit is called before the DOM bindings are updated for the current component. And the ngAfterViewInit is called after. That is why it works in one case and doesn't work in the other.

This article will help you understand the error better - Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error.

like image 43
Max Koretskyi Avatar answered Oct 15 '22 10:10

Max Koretskyi