Fixing Expression Has Changed After It Was Checked Error in Angular
In this post, I want to show you how to fix a common Angular error: Expression has changed after it was checked
.
During development, you might encounter this error at some point. Here is a hint: "The previous value was 'loading...', but the current value is 'all done loading'." As you can see, this occurs in the AppComponent
.
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, CommonModule],
template: '<div>{{message}}</div>',
styleUrl: './app.component.css',
})
export class AppComponent {
message: string = 'loading...'
ngAfterViewInit() {
this.message = 'all done loading'
}
}
Here is our AppComponent
with a single rendered variable message
. By default, the value is loading...
and later we change it to all done loading
.
With such little code, we get this error, and it is confusing for most people.
The first thing I want to mention here is that this error is rendered only in development mode. If we build our Angular application for production, we never see this error. Now we must understand why this error occurs at all.
In development mode, when Angular runs its change detection cycle, it does so twice, directly one after another. So we are getting two cycles.
A lot of people think that this is completely useless. But it is not useless.
Angular checks that all values remain the same and are not changed.
If they are changed between checks, then you have some nasty mutations or side effects that should not be there.
Inside our code, we are changing the message
inside the ngAfterViewInit
hook. The main problem is that we changed the value, but the change detection cycle was not triggered. This change is not reflected in the markup. Angular compares the bindings of all properties and sees that this property was changed. This is exactly what this error indicates.
The most important thing to remember is that if you have anything that changes bindings inside the template, you must run a new change detection cycle afterward.
Ways to Fix
Let's take a look at all the ways we can fix this error. The first way is to update our message not inside ngAfterViewInit
but inside ngAfterContentChecked
. This fixes our error.
This helps because ngAfterViewInit
happens after DOM rendering, but ngAfterContentChecked
is done before ngAfterViewInit
. So, we update our value before the DOM update happens. It is similar to writing our logic inside ngOnInit
or inside a constructor
.
Another way to fix it is by manually running change detection.
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, CommonModule],
template: '<div>{{message}}</div>',
styleUrl: './app.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
message: string = 'loading...'
ngAfterViewInit() {
this.message = 'all done loading'
}
}
Here we just added OnPush
change detection and left the code as it was previously. We won't get any error, but our message won't be changed either.
So our changes are not reflected inside the view.
With
OnPush
you will get fewer errors, but often you need to trigger the change detection cycle manually.
For example, in our case here.
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, CommonModule],
template: '<div>{{message}}</div>',
styleUrl: './app.component.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
message: string = 'loading...'
cdr = inject(ChangeDetectionRef)
ngAfterViewInit() {
this.message = 'all done loading'
this.cdr.detectChanges()
}
}
Now it works completely correctly in the browser. It's the best fix for this specific use case.
But we can also do it differently. If we just wrap our code with setTimeout
, it will also help.
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, CommonModule],
template: '<div>{{message}}</div>',
styleUrl: './app.component.css',
})
export class AppComponent {
message: string = 'loading...'
ngAfterViewInit() {
setTimeout(() => {
this.message = 'all done loading'
})
}
}
It is not the cleanest solution, but it will also work.
One More Example
Now I want to show you exactly the same error but with different code.
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, CommonModule],
template: ` <div>I'm {{ loading$ | async }} {{ response$ | async }}</div> `,
styleUrl: './app.component.css',
})
export class AppComponent {
loading$ = new BehaviorSubject(true);
response$ = of('response').pipe(
tap(() => this.loading$.next(false)),
);
}
The logic is similar, but here we use RxJS and async pipes. We have a loading$
stream and a response$
stream. The problem here is that immediately after setting the response, we change the loading state.
The error is the same, but the values change from true to false.
To fix this error, we must apply the same fix as we did with setTimeout
, but in an RxJS way.
response$ = of('response').pipe(
delay(0),
tap(() => this.loading$.next(false)),
);
Now our tap
will be asynchronous, which eliminates the problem.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done