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

3

18

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.

Displace answered 10/7, 2017 at 12:31 Comment(2)
This will helpful to understand detectChanges from here https://mcmap.net/q/107848/-what-39-s-the-difference-between-markforcheck-and-detectchangesCollagen
This article will help you understand the error better - Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error.Lindner
L
7

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.

Lindner answered 10/7, 2017 at 12:55 Comment(3)
Thanks for the detailed answer. That has helped me a lot!Displace
you're welcome, you definitely will want to read the articles I mentioned in the answer to avoid these errors in the future. I also referenced an article about forwardRef under one of your answersLindner
@DanielGrima, thanks, make sure to follow me for more in depth articles)Lindner
M
14

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

Martamartaban answered 10/7, 2017 at 12:48 Comment(5)
@Maximus what do you mean?Martamartaban
@Maximus I also suggested ngOnInit towards the bottom. Depends on whether user needs to get hold of something in the view.Martamartaban
@Maximus I don't know whether it does work there since we have only the excerpt in the OPMartamartaban
I added information why it works if you move to the ngOnInitLindner
Won't this just trigger another changeDetection after the setTimeout fires? Why would it be less expensive than triggering the change detection manually?Inaptitude
L
7

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.

Lindner answered 10/7, 2017 at 12:55 Comment(3)
Thanks for the detailed answer. That has helped me a lot!Displace
you're welcome, you definitely will want to read the articles I mentioned in the answer to avoid these errors in the future. I also referenced an article about forwardRef under one of your answersLindner
@DanielGrima, thanks, make sure to follow me for more in depth articles)Lindner
C
2

Use detectChanges() when you've updated the model after angular has run it's change detection, or if the update hasn't been in angular world at all.

Collagen answered 10/7, 2017 at 12:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.