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

Daniel Grima picture Daniel Grima · Jul 10, 2017 · Viewed 8.3k times · Source

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.

Answer

BeetleJuice picture BeetleJuice · Jul 10, 2017

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