How to manage Angular2 "expression has changed after it was checked" exception when a component property depends on current datetime

AngularTypescriptTimeStylesComponents

Angular Problem Overview


My component has styles that depend on current datetime. In my component I've got the following function.

  private fontColor( dto : Dto ) : string {
    // date d'exécution du dto
    let dtoDate : Date = new Date( dto.LastExecution );

	(...)

    let color =  "hsl( " + hue + ", 80%, " + (maxLigness - lightnessAmp) + "%)";

    return color;
  }

lightnessAmp is calculated from the current datetime. The color changes if dtoDate is in the last 24 hours.

The exact error is the following:

> Expression has changed after it was checked. Previous value: 'hsl( 123, 80%, 49%)'. Current value: 'hsl( 123, 80%, 48%)'

I know the exception appear in development mode only at the moment the value is checked. If the checked value is different of the updated value, the exception is thrown.

So I tried to update the current datetime at each lifecycle in the following hook method to prevent the exception:

  ngAfterViewChecked()
  {
    console.log( "! changement de la date du composant !" );
    this.dateNow = new Date();
  }

...but without success.

Angular Solutions


Solution 1 - Angular

Run change detection explicitly after the change:

import { ChangeDetectorRef } from '@angular/core';

constructor(private cdRef:ChangeDetectorRef) {}

ngAfterViewChecked()
{
  console.log( "! changement de la date du composant !" );
  this.dateNow = new Date();
  this.cdRef.detectChanges();
}

Solution 2 - Angular

TL;DR

ngAfterViewInit() {
    setTimeout(() => {
        this.dateNow = new Date();
    });
}

Although this is a workaround, sometimes it's really hard to solve this issue in any nicer way, so don't blame yourself if you are using this approach. That's okay.

Examples: The initial issue [link], Solved with setTimeout() [link]


How to avoid

In general this error usually happens after you add somewhere (even in parent/child components) ngAfterViewInit. So first question is to ask yourself - can I live without ngAfterViewInit? Perhaps you move the code somewhere ( ngAfterViewChecked might be an alternative).

Example: [link]


Also

Also async stuff in ngAfterViewInit that affects DOM might cause this. Also can be solved via setTimeout or by adding the delay(0) operator in the pipe:

ngAfterViewInit() {
  this.foo$
    .pipe(delay(0)) //"delay" here is an alternative to setTimeout()
    .subscribe();
}

Example: [link]


Nice Reading

Good article about how to debug this and why it happens: link

Solution 3 - Angular

As mentioned by @leocaseiro on github issue.

> I found 3 solutions for those who are looking for easy fixes. > > 1) Moving from ngAfterViewInit to ngAfterContentInit > > 2) Moving to ngAfterViewChecked combined with ChangeDetectorRef as > suggested on #14748 (comment) > > 3) Keep with ngOnInit() but call ChangeDetectorRef.detectChanges() after > your changes.

Solution 4 - Angular

Here you go two solutions!


1. Modify ChangeDetectionStrategy to OnPush

For this solution, you're basically telling angular:

> Stop checking for changes; I'll do it only when I know is necessary

Modify your component so it'll use ChangeDetectionStrategy.OnPush like this:

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {
    // ...
}

With this, things don’t seem to work anymore. That's because from now on you'll have to make Angular call the detectChanges() manually.

this.cdr.detectChanges();

If you're interested, please check this article. It helped me understand how ChangeDetectionStrategy works.


2. Understanding ExpressionChangedAfterItHasBeenCheckedError

Please check this video about the issue (is great!). Also, here is a small extract from this article about the causes for this error, I've tried to include only the parts that helped me to understand this.

The full article shows real code examples about every point shown here.

The root cause is angular lifecycle's:

> After each operation Angular remembers what values it used to perform > an operation. They are stored in the oldValues property of the > component view. > > After the checks have been done for all components Angular then starts > the next digest cycle but instead of performing operations it compares the current values with the ones it remembers from > the previous digest cycle.

The following operations are being checked at digest cycles:

> check that values passed down to the child components are the same as > the values that would be used to update properties of these components > now.

> check that values used to update the DOM elements are the same as > the values that would be used to update these elements now perform the > same.

> checks for all child components

And so, the error is thrown when the compared values are different., blogger Max Koretskyi stated:

> The culprit is always the child component or a directive.

And finally here are some real-world samples that usually cause this error:

  • Shared services (example)
  • Synchronous event broadcasting (example)
  • Dynamic component instantiation (example)

In my case, the problem was a dynamic component instantiation.

Also, from my own experience, I strongly recommend everyone to avoid the setTimeout solution, in my case caused an "almost" infinite loop (21 calls which I'm not willing to show you how to provoke them),

I would recommend always keeping in mind the Angular life cycle's so you can take into account how they would be affected every time you modify another component's value. With this error Angular is telling you:

> You're maybe doing this the wrong way, are you sure you're right?

The same blog also says:

> Often, the fix is to use the right change detection hook to create a dynamic component


A short guide for me is to consider at least the following things while coding:

(I'll try to complement it over time):

  1. Avoid modifying parent component values from its child's components, instead: modify them from their parent.
  2. When you use @Input and @Output directives try to avoid triggering lifecycle changes unless the component is completely initialized.
  3. Avoid unnecessary calls of this.cdr.detectChanges(); they can trigger more errors, especially when you're dealing with a lot of dynamic data
  4. When the use of this.cdr.detectChanges(); is mandatory make sure that the variables (@Input, @Output, etc) being used are filled/initialized at the right detection hook (OnInit, OnChanges, AfterView, etc)
  5. When possible, remove rather than hide, this is related to point 3 and 4. (same quote for angulardart)
  6. Avoid any kind of logic inside setters annotated with @Input, setters are executed previously to ngAfterViewInit so it'll easily trigger the issue. In case you need to, its better of to put that logic inside the ngOnChanges method.
Also

If you want to fully understand Angular Life Hook I recommend you to read the official documentation here: https://angular.io/guide/lifecycle-hooks

Solution 5 - Angular

In our case we FIXED by adding changeDetection into the component and call detectChanges() in ngAfterContentChecked, code as follows

@Component({
  selector: 'app-spinner',
  templateUrl: './spinner.component.html',
  styleUrls: ['./spinner.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpinnerComponent implements OnInit, OnDestroy, AfterContentChecked {

  show = false;

  private subscription: Subscription;

  constructor(private spinnerService: SpinnerService, private changeDedectionRef: ChangeDetectorRef) { }

  ngOnInit() {
    this.subscription = this.spinnerService.spinnerState
      .subscribe((state: SpinnerState) => {
        this.show = state.show;
      });
  }

  ngAfterContentChecked(): void {
      this.changeDedectionRef.detectChanges();
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}

Solution 6 - Angular

A small work around I used many times

Promise.resolve(data).then(() => {
    console.log( "! changement de la date du composant !" );
    this.dateNow = new Date();
    this.cdRef.detectChanges();
});

Solution 7 - Angular

Move your code from ngAfterViewInit to ngAfterContentInit.

The view is initialized after the content and ngAfterViewInit() is therefore called after ngAfterContentInit()

Solution 8 - Angular

Use a default form value to avoid the error.

Instead of using the accepted answer of applying detectChanges() in ngAfterViewInit() (which also solved the error in my case), I decided instead to save a default value for a dynamically required form field, so that when the form is later updated, it's validity is not changed if the user decides to change an option on the form that would trigger the new required fields (and cause the submit button to be disabled).

This saved a tiny bit of code in my component, and in my case the error was avoided altogether.

Solution 9 - Angular

I think the best and cleanest solution you can imagine is this:

@Component( {
  selector: 'app-my-component',
  template: `<p>{{ myData?.anyfield }}</p>`,
  styles: [ '' ]
} )
export class MyComponent implements OnInit {
  private myData;

  constructor( private myService: MyService ) { }

  ngOnInit( ) {
    /* 
      async .. await 
      clears the ExpressionChangedAfterItHasBeenCheckedError exception.
    */
    this.myService.myObservable.subscribe(
      async (data) => { this.myData = await data }
    );
  }
}

Tested with Angular 5.2.9

Solution 10 - Angular

Although there are many answers already and a link to a very good article on change detection, I wanted to give my two cents here. I think the check is there for a reason so I thought about the architecture of my app and realized that the changes in the view can be dealt with by using BehaviourSubject and the correct lifecycle hook. So here's what I did for a solution.

  • I use a third-party component (fullcalendar), but I also use Angular Material, so although I made a new plugin for styling, getting the look and feel was a bit awkward because customization of the calendar header is not possible without forking the repo and rolling your own.

  • So I ended up getting the underlying JavaScript class, and need to initialize my own calendar header for the component. That requires the ViewChild to be rendered befor my parent is rendered, which is not the way Angular works. This is why I wrapped the value I need for my template in a BehaviourSubject<View>(null):

     calendarView$ = new BehaviorSubject<View>(null);
    

Next, when I can be sure the view is checked, I update that subject with the value from the @ViewChild:

  ngAfterViewInit(): void {
    // ViewChild is available here, so get the JS API
    this.calendarApi = this.calendar.getApi();
  }

  ngAfterViewChecked(): void {
    // The view has been checked and I know that the View object from
    // fullcalendar is available, so emit it.
    this.calendarView$.next(this.calendarApi.view);
  }

Then, in my template, I just use the async pipe. No hacking with change detection, no errors, works smoothly.

Please don't hesitate to ask if you need more details.

Solution 11 - Angular

I got that error because I declared a variable and later wanted to
changed it's value using ngAfterViewInit

export class SomeComponent {
    
    header: string;

}

to fix that I switched from

ngAfterViewInit() { 
    
    // change variable value here...
}

to

ngAfterContentInit() {

    // change variable value here...
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAnthony Breneli&#232;reView Question on Stackoverflow
Solution 1 - AngularGünter ZöchbauerView Answer on Stackoverflow
Solution 2 - AngularSergei PanfilovView Answer on Stackoverflow
Solution 3 - AngularcandidJView Answer on Stackoverflow
Solution 4 - Angularluiscla27View Answer on Stackoverflow
Solution 5 - AngularCharitha GoonewardenaView Answer on Stackoverflow
Solution 6 - AngularKhateeb321View Answer on Stackoverflow
Solution 7 - AngularAchraf FaroukyView Answer on Stackoverflow
Solution 8 - AngularT. BulfordView Answer on Stackoverflow
Solution 9 - AngularJavierFuentesView Answer on Stackoverflow
Solution 10 - AngularthomiView Answer on Stackoverflow
Solution 11 - Angularuser12163165View Answer on Stackoverflow