[Debugging] Expression has changed after it was checked

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
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.
Info
Channel: Angular
Views: 52,864
Rating: undefined out of 5
Keywords: web development, angular, typescript, javascript, debugging
Id: O47uUnJjbJc
Channel Id: undefined
Length: 6min 23sec (383 seconds)
Published: Fri Jan 15 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.