ExpressionChangedAfterItHasBeenCheckedError. You'll encounter this error if your code changes a value after change detection has been run and the view has been built. You'll only see this error in development
because Angular runs an additional change detection check to catch errors like
this that can cause erratic UI behavior. The extra check ensures the app is stable and that
all data updates have been reflected in the views. There are a variety of reasons the view might
be left in an inconsistent state, such as code that updates the view during the AfterViewInit
lifecycle hook or when change detection triggers itself in an infinite loop, like a method that
returns a different value each time, or a child component that changes the bindings on its parent. Let's start by looking at a simple reproduction and solution. Then, we'll take a more detailed
look at Angular's change detection to understand why this error happens and why it's important.
Here in the app component template we're using the ngIf directive with a loading boolean.
In the model, or our component type script, we give it a default value of true. Then, using
the AfterViewInit lifecycle hook we flip the value to false when it's done loading.
But when we run this code we see ExpressionChangedAfterItHasBeenCheckedError:
Previous value: false, Current value: true. Now in a more complex app it might not be
clear where this error originates from, but you can always assume that it has
something to do with a binding in the template. In the stack trace you'll find a link to the
source map for the component template that caused the error and it takes us directly
to the line of code that caused the issue, which is our ngIf binding to the loading boolean.
What the error is trying to tell us is that the loading value has changed after the change
detection cycle has completed, but what exactly is wrong with our code in this case?
The short answer is that we're using the wrong lifecycle hook.
If we move our code from AfterViewInit to OnInit the error goes
away and everything works perfectly. In other words, if you find yourself updating
values in AfterViewInit there's a good chance that a simple refactor to OnInit or the
component constructor will fix the issue. Okay, at this point we know we used the wrong
lifecycle hook and we can fix it by refactoring, but to really understand why let's do a quick
review of how change detection works in Angular. The goal of change detection is to
keep the model, your typescript code, in sync with the template, your html.
And it does so by looking for data changes in the component tree from top to bottom.
First it checks the parent, then the first child, second child and so on, but if we update a
binding on the parent after it has already been checked Angular will throw the error.
Now what we have here is a simplified breakdown of Angular's life cycle, or the steps
it performs when a component is first initialized. First it updates the bindings like
the ngIf directive in the template. Then it runs the OnInit lifecycle
hook, updates the DOM, then runs change detection for the child component.
Notice how the final step is AfterViewInit, and more importantly that it
runs after change detection. Basically any code that runs here
should not attempt to update the view and that was the root of the problem in this case.
Refactoring to OnInit works great for initial values, but if that doesn't fix the issue there
are a few other ways you may have encountered this error, along with additional ways to fix it.
In the component here, we're using ViewChild to grab an element from the DOM, but this
element won't be available until the AfterViewInit lifecycle hook is called.
But what if we can't update the state of the component until after we
have the ViewChild element? If we can't refactor to ngOnInit,
we have a couple of other options. An approach you'll often see on StackOverflow
answers is to make the update asynchronous. When we make the update async, it'll be
picked up on the next change detection check and prevent the error from occurring.
We can make it asynchronous by wrapping it in a set timeout with a delay of zero. That'll put
the update in the next macro task queue of the Javascript event loop. Alternatively we can use
a promise that resolves immediately, then run the update in its callback. This code will achieve the
same result, the only subtle difference is that it runs on the micro task queue before the end of
the current iteration in the browser's event loop. Making the update asynchronous can
work, however it's very implicit and should really only be used as a last resort.
It's not clear why we make this code asynchronous unless you understand the nuances of Angular
change detection and the browser's event loop. Luckily Angular provides us with a more direct
and explicit way to trigger change detection. We can manually trigger it by injecting the change
detector ref in the constructor of the component. We can then use it to manually run change
detection by calling the detect changes method. This will tell Angular to check the view and
its children, in which case it will notice that our loading state has changed, giving
us yet another way to address the error. Now an entirely different way you might encounter
this error is when you have a method, usually a getter, that doesn't return a predictable value,
which can cause an infinite change detection loop. Take for example this getter in our
component that returns a random number. If we try to use this value in the
template, Angular will get a different value each time it's checked.
The solution in this case is to make the method return a consistent
value based on the state of the component. In other words getters should be derived
directly from the component state and not values that change constantly
like timestamps or random numbers. Now let's take a look at one more example, where
we have both a parent and child component at play. The app component the parent contains the
loading state just like our previous examples, but instead of running the update from the
parent component, we'll make it happen from a child component with a custom event.
In the item component, the child, we're using the @Output decorator to create
a custom event along with an event emitter. Then during ngOnInit we'll go ahead and
emit the event with a value of true. Then back in the app component template we'll
go ahead and declare the item component. When it fires, we'll set
the loading value to false. The end result is a situation where we have a
child component updating the parent after change detection has already run on the parent,
and once again that produces the error. A potential solution in this case would be to
move the loading state into the child component and if that's not possible you might consider
moving this state to a shared service where it can be injected in multiple components. Let's finish up by doing a quick recap. The ExpressionChangedAfterItHasBeenCheckedError
occurs because a value in the template has been updated after change detection has finished.
Debug it by first finding the template in the stack trace.
From there you can analyze your code to determine where the value is being updated and use one of
the methods covered in this video to address it, such as refactoring to the OnInit lifecycle
hook, using change detector ref manually, making getters item potent, or making
your updates async as a last resort. Refer to the Angular documentation
for additional details and examples.