'How to manage Angular2 "expression has changed after it was checked" exception when a component property depends on current datetime
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.
Solution 1:[1]
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:[2]
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:[3]
As mentioned by @leocaseiro on github issue.
I found 3 solutions for those who are looking for easy fixes.
1) Moving from
ngAfterViewInit
tongAfterContentInit
2) Moving to
ngAfterViewChecked
combined withChangeDetectorRef
as suggested on #14748 (comment)3) Keep with ngOnInit() but call
ChangeDetectorRef.detectChanges()
after your changes.
Solution 4:[4]
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):
- Avoid modifying parent component values from its child's components, instead: modify them from their parent.
- When you use
@Input
and@Output
directives try to avoid triggering lifecycle changes unless the component is completely initialized. - Avoid unnecessary calls of
this.cdr.detectChanges();
they can trigger more errors, especially when you're dealing with a lot of dynamic data - 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
) - When possible, remove rather than hide, this is related to point 3 and 4. (same quote for angulardart)
- Avoid any kind of logic inside
setters
annotated with@Input
, setters are executed previously tongAfterViewInit
so it'll easily trigger the issue. In case you need to, its better of to put that logic inside thengOnChanges
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:[5]
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:[6]
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:[7]
Move your code from ngAfterViewInit
to ngAfterContentInit
.
The view is initialized after the content and ngAfterViewInit()
is therefore called after ngAfterContentInit()
Solution 8:[8]
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:[9]
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:[10]
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 aBehaviourSubject<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:[11]
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...
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | rmcsharry |
Solution 2 | |
Solution 3 | Pipo |
Solution 4 | rofrol |
Solution 5 | |
Solution 6 | |
Solution 7 | luiscla27 |
Solution 8 | rofrol |
Solution 9 | |
Solution 10 | thomi |
Solution 11 |