Causes and workarounds
We know that the error happens when the values returned by an expression during the regular change detection run
and the following verification cycle are different. To minimize the chance of this happening,
it’s better to do most application state updates outside of change detection loop.
UI event handlers, that run before Angular runs change detection,
is a great place for the such state updating logic. Other possible strategies include using
a hook that runs before side effects that lead to the error happen or even tweaking the order of elements in the template.
Sometimes, however, we cannot use any of the suggested strategies.
For example, we might need to wait until the DOM is updated, possibly to take measurements or attach a dynamic component.
The updated DOM is available in the AfterViewChecked
hook,
but this is also the hook that’s called after all side-effects (DOM/property updated etc) have finished running for the entire components tree.
So if your logic updates any expressions inside the template, you’ll get the error.
This means that updating an application state inside this hook is prone to causing the error.
Another common scenario that is prone to running update logic inside the change detection
and resulting in the error is loop is using directives with input bindings.
By convention, inputs is the default communication mechanism for directives.
Because of that, business or presentation logic that leads to state updates is triggered in response
to those input properties updates. This setup is also very likely to accidentally cause the error.
The error might also pop up because of the indirect update of the ancestor’s
property through a shared service or global synchronous event dispatch.
The code that leads to this may be placed inside relatively safe hooks like ngOnInit
or input binding setters.
How do we deal with those?
First, there’s always an option to simply ignore the error.
In fact, sometimes once you make sure the error isn’t critical,
i.e. doesn’t lead to incorrect conclusions or prevents users from accomplishing their tasks UX wise,
it’s OK to let the error hang on there. An example of such case would be a binding that implements focus for an input.
If the error pops up when the input receives the focus, it could be ignored.
However if you have time, it’s always a good idea to track down the leading cause and apply a workaround.
In the next section we’ll see how to pinpoint the leading cause of the error using debugging techniques.
For workarounds, there are basically two options:
- delay the actual update until after the change detection cycle is finished
- run local change detection cycle to bring the application state and DOM into sync before
checkNoChanges
error
Another less common option but very straightforward is to directly update
the DOM through the API without using Angular’s binding mechanism. For example,
this is how
Angular material does it for matChip
.
Let’s see here how we can apply those workarounds for the error prone scenarios we outlined above.
We’ll start with the one that occurs most often – mischievous code inside AfterViewChecked
hook that results
in application state update.
State updates inside AfterViewChecked
hook
The AfterViewChecked
hook allows developers to post-process the results of side-effects,
such as DOM updates, after the view has been checked. Since the hook is executed after the component has been checked,
updating an application state inside this hook often leads to “changed after checked” error.
What’s important to understand is that by itself updating the application state won’t trigger the error.
It’s only if the logic inside the ngAfterViewChecked
updates properties that are used in expressions
evaluated during change detection for side-effects.
Here’s a very straightforward example that illustrates this.
We have a property numberOfChildren
that keeps track of how many child components currently displayed on the screen.
When a user clicks on the button, we show <v1-cmp>
through ngIf
directive.
Angular runs change detection and the property numberOfChildren
is updated inside ngAfterViewChecked
hook:
@Component({
selector: 'va-cmp',
template: `
<button (click)="toggleShowChild()">Show child</button>
<v1-cmp *ngIf="visible"></v1-cmp>
`,
})
export class VA {
@ViewChildren(V1) children: QueryList<any> = null;
visible = false;
numberOfChildren = 0;
toggleShowChild() {
this.visible = true;
}
ngAfterViewChecked() {
this.numberOfChildren = this.children.length;
}
}
@Component({
selector: 'v1-cmp',
template: `I am child V1 component `,
})
export class V1 {}
We run this code, and all works fine:
There’s no error. However, let’s now show a message on the screen that tells users how many child components is currently shown.
For that, we’ll add an interpolation {{children.length}}
to the template. That’s the only change we make:
@Component({
selector: 'va-cmp',
template: `
<h1>I have {{numberOfChildren}} child components</h1>
<button (click)="toggleShowChild()">Show child</button>
<v1-cmp *ngIf="visible"></v1-cmp>
`
})
export class VA {...}
When a user clicks on the button, we make <v1-cmp>
visible and expect the text “I have 1 child components” on the screen.
But this time when we run the code, it produces the “changed after checked” error:
What is more critical it leads to an inconsistent state, where the text says we have 0
child components
but we can see one V1
rendered on the screen.
The error happens because Angular remembered value 0
for the length
expression when
it executed the template function for the VA
component. A template function is processed before ngAfterViewChecked
hook according to the
operations order.
When it executed the template function second time during checkNoChanges
phase, the expression yielded value 1
,
which caused the error.
There was no error before we introduced the interpolation to the template because this template function
didn’t have any binding for which Angular remembered and compared values for expressions.
This expression in the template of VA
would result in the same error regardless of the mechanism that we use to show the child component.
For example, instead of showing V1
using ngIf
, we could add the component dynamically using a view container like this:
@Component({
selector: 'v2-cmp',
template: `
<h1>I have {{ numberOfChildren }} child components</h1>
<button (click)="toggleShowChild()">Show child</button>
<ng-container #vc></ng-container>
`,
})
export class V2 {
@ViewChild('vc', { read: ViewContainerRef }) vc;
numberOfChildren = 0;
toggleShowChild() {
this.vc.createComponent(V1);
}
ngAfterViewChecked() {
this.numberOfChildren = this.vc.length;
}
}
which would produce the error again because of the interpolation in a template.
Workarounds
To fix the error, we can either directly update the DOM or apply one of these techniques:
- run local change detection cycle through
detectChanges
error - delay the actual update until after the change detection cycle is finished
Let’s first see how to refactor the application so that we update the DOM without using bindings.
Here’s how it would look like:
@Component({
selector: 'va-cmp',
template: `
<h1 #text></h1>
<button (click)="toggleShowChild()">Show child</button>
<v1-cmp *ngIf="visible"></v1-cmp>
`,
})
export class VA {
@ViewChildren(V1) children: QueryList<any> = null;
@ViewChild('text') text: ElementRef = null;
visible = false;
numberOfChildren = 0;
toggleShowChild() {
this.visible = true;
}
ngAfterViewChecked() {
this.numberOfChildren = this.children.length;
this.text.nativeElement.textContent = `I have ${this.numberOfChildren} child components`;
}
}
If your target platform is a browser, there’s nothing wrong with this approach. Indeed,
this is how
Angular material prevents the error for MatChipInput
component:
export class MatChipInput {
setDescribedByIds(ids: string[]): void {
const element = this._elementRef.nativeElement;
// Set the value directly in the DOM since this binding
// is prone to "changed after checked" errors.
if (ids.length) {
element.setAttribute('aria-describedby', ids.join(' '));
} else {
element.removeAttribute('aria-describedby');
}
}
}
Running local change detection
Let’s now see what happens if we use workarounds. We’ll start with detectChanges
:
@Component({
selector: 'va-cmp',
template: `
<h1>I have {{ numberOfChildren }} child components</h1>
<button (click)="toggleShowChild()">Show child</button>
<v1-cmp *ngIf="visible"></v1-cmp>
`,
})
export class VA {
@ViewChildren(V1) children: QueryList<any> = null;
constructor(public cdRef: ChangeDetectorRef) {}
visible = false;
numberOfChildren = 0;
toggleShowChild() {
this.visible = true;
}
ngAfterViewChecked() {
this.numberOfChildren = this.children.length;
this.cdRef.detectChanges();
}
}
This time, when we run the application, we’ll get no error.
Calling detectChanges
from inside ngAfterViewChecked
basically re-runs refreshView
for the current component.
After the first run of refreshView
, Angular remembers the value 0
for the {{numberOfChildren}}
expression.
Inside the ngAfterViewChecked
hook we update the value for the expression to 1
.
Those are inconsistent and would lead to the error during the following checkNoChanges
verification loop.
However, before Angular runs this verification loop, we trigger another cycle of detectChanges
inside the ngAfterViewChecked
right after we update the numberOfChildren
property.
This will make Angular remember value 1
for the binding because {{numberOfChildren}}
evaluates to 1
after the first run of change detection.
When Angular runs its verification loop, {{numberOfChildren}}
again evaluates to the value of 1
which matches the remembered value. There’s no inconsistency anymore.
Delaying the update
The other option we have is to delay the actual update until after the change detection cycle is finished.
Most often, to delay the update developers use setTimeout
. Here’s how the refactored version will look like:
@Component({
selector: 'va-cmp',
template: `
<h1>I have {{ numberOfChildren }} child components</h1>
<button (click)="toggleShowChild()">Show child</button>
<v1-cmp *ngIf="visible"></v1-cmp>
`,
})
export class VA {
@ViewChildren(V1) children: QueryList<any> = null;
visible = false;
numberOfChildren = 0;
toggleShowChild() {
this.visible = true;
}
ngAfterViewChecked() {
setTimeout(() => {
this.numberOfChildren = this.children.length;
});
}
}
If we run this example, we won’t see any error.
The way it works, is that Angular finishes current change detection cycle without the error as the {{numberOfChildren}}
expression evaluates to 0
during the regular check and the verification loop. Using the setTimeout
we schedule another run of JS, where we update the numberOfChildren
property using the value for ViewChildren
from the previous change detection run. After application code finishes executing, Angular runs another change detection cycle, where numberOfChildren
property evaluates to 1
during the regular change detection run and the subsequent verification loop.
Surprisingly, though, this use of setTimeout
will lead to an infinite loop of change detection runs.
It’s hard to notice because the application doesn’t freeze.
Scheduling a macrotask gives the browser room for handling UI events such as clicks.
The infinite loops happens because each setTimeout
call triggers change detection,
and during each change detection run the ngAfterViewChecked
is executed, which calls setTimeout
again.
We could fix it by not scheduling setTimeout
if the value hasn’t changed:
@Component({...})
export class VA {
@ViewChildren(V1) children: QueryList<any> = null;
visible = false;
numberOfChildren = 0;
toggleShowChild() {
this.visible = true;
}
ngAfterViewChecked() {
if (this.numberOfChildren !== this.children.length) {
setTimeout(() => {
this.numberOfChildren = this.children.length;
});
}
}
This will prevent an infinite loop. When you apply a workaround, don’t forget to put an explanatory comment.
This is, for example,
how it’s done
in Angular material codebase.
While setTimeout
is the approach used most often, a resolved promise Promise.resolve()
might be a better option because it saves a browser some painting work. That’s the approach Angular uses in its
forms package.
The setTimeout
function schedules a macrotask then will be executed in the next VM turn.
Instead of a macrotask Promise.resolve()
creates a microtask.
The microtask queue is processed after the current synchronous code has finished executing.
It means the code inside then()
will run in the current VM turn, but after the current change detection run is finished.
In short, the difference between using setTimeout
or Promise.resolve()
is when the delayed update will happen,
before or after JS (scripting) yields to a browser so that it can run its tasks related to rendering and painting.
To learn more about micro and macro tasks in Angular you can read
I reverse-engineered Zones (zone.js) and here is what I’ve found.
Here’s how we could use it in our example:
@Component({
selector: 'va-cmp',
template: `
<h1>I have {{ numberOfChildren }} child components</h1>
<button (click)="toggleShowChild()">Show child</button>
<v1-cmp *ngIf="visible"></v1-cmp>
`,
})
export class VA {
@ViewChildren(V1) children: QueryList<any> = null;
visible = false;
numberOfChildren = 0;
toggleShowChild() {
this.visible = true;
}
ngAfterViewChecked() {
if (this.numberOfChildren !== this.children.length) {
Promise.resolve().then(() => {
this.numberOfChildren = this.children.length;
});
}
}
}
The way it works, is that Angular finishes current change detection cycle without the error as the numberOfChildren
expression evaluates to 0
during the regular check and the verification loop. Using the Promise.resolve()
we add a microtask that will run before JS yields to the browser. After that, the mechanics of preventing the error is similar to using setTimeout
. We update the numberOfChildren
property using the value for ViewChildren
from the previous change detection run. After application code finishes executing, Angular runs another change detection cycle, where numberOfChildren
property evaluates to 1
during the regular change detection run and the subsequent verification loop.
A very important difference between running local change detection cycle and delaying the update
is that a delayed update will lead to the application wide change detection,
while calling detectChanges
will trigger local change detection.
This means that most of the time detectChanges
is the preferred solution.
Ignoring the error
One of the approaches I suggested in the intro is to simply ignore the error.
For the example that we used above, ignoring the error isn’t a good option, because it results in the inconsistent state –
it says you have 0 child component but on the screen you clearly see one component rendered:
Indirect update of the ancestor’s property through directives
In real-life applications, an ancestor’s property used in a binding expression is usually updated indirectly. That can happen through a number of different mechanisms, most commonly through a shared service or a global synchronous event dispatch. Let’s see an example of that.
Suppose we need to implement a requirement to show a modal dialog when a user clicks on a button. When we show a modal dialog, we want scrollbars to disappear. This is a rather elaborate setup that involves a bunch of actors:
- a root component that hosts the modal dialog
- a directive that is used to show a modal
- a component that implements the business logic and uses the directive to show the modal
- a modal service that’s shared between the root component and the modal showing directive
Instead of implementing the logic that actually renders a modal dialog,
we’ll simply change the background of the root component that will indicate that the modal is shown.
Here’s how the UI looks like:
Here’s how the code looks:
@Component({
selector: 'exp-root',
providers: [DialogService],
template: `
<div class="viewport">
<exp-desc-cmp></exp-desc-cmp>
</div>
`,
styles: [
`
.viewport {
height: 100%;
margin-top: calc(100% / 2);
padding: 20px;
}
:host {
height: 100vh;
overflow: auto;
}
:host.modal {
overflow: hidden;
background: #2569af4d;
}
`,
],
})
export class ExpRootComponent {
@HostBinding('class.modal') public modal = false;
constructor(dialogService: DialogService) {
dialogService.onDialogsChange((dialogs: any) => {
this.modal = dialogs.length > 0;
});
}
}
export class DialogService {
dialogs = [];
notification = new Subject();
open(options) {
const dlg = { ...options };
this.dialogs.push(dlg);
this.notification.next(this.dialogs);
return dlg;
}
close(dlg) {
const i = this.dialogs.findIndex((d) => dlg === d);
if (i === -1) return;
this.dialogs.splice(i, 1);
this.notification.next(this.dialogs);
}
onDialogsChange(fn) {
this.notification.subscribe(fn);
}
}
@Directive({
selector: '[dialog]',
})
export class DialogDirective {
constructor(private dialogService: DialogService) {}
dialog = null;
@Input('dialog') set open(open: boolean) {
if (open) {
this.dialog = this.dialogService.open({});
} else {
this.dialogService.close(this.dialog);
}
}
}
@Component({
selector: 'exp-desc-cmp',
template: `
<div>
<button (click)="show = !show">{{ show ? 'close' : 'open' }}</button>
<div [dialog]="show"></div>
</div>
`,
})
export class ExpDescendant {
show = false;
}
Basically, we’re using the directive DialogDirective
to declarative render the modal dialog
from inside the ExpDescendant
that implements some business logic.
A root component ExpRootComponent
handles the UI part for the modal, and usually it would use portals
to render the modal outside of Angular tree. The service is simply a communication mechanism between
the ExpRootComponent
and whichever component wants to present a modal dialog.
The problem with this setup is when we run it the “change after check” error pops up.
Here’s what it says:
ERROR Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.
Previous value for ‘modal’: ‘false’. Current value: ‘true’.
It tells us that modal
binding caused the problem, and in the callstack we can see that this error originates inside the host binding of the ExpRootComponent
:
This kind of setup may be a bit hard to debug and pinpoint the leading cause.
I’ll show you a few techniques in the chapter on debugging the “changed after check” error.
Since our focus here is to explore workarounds, not debug the cause, I’ll tell you what happens here.
Here’s the order of operations that lead to the error. When users click on the button inside ExpDescendant
, an event handler updates the property show
from false
to true
. After that Angular runs change detection.
- For
ExpRootComponent
component it evaluates the expressionmodal
for the@HostBinding
. The expression yields valuefalse
because thedialogs
array is empty (andfalse
is its initial value) and this value is remembered - For
ExpDescendant
it updates input binding with the valuetrue
and the property setter on update callsdialogService.open()
. – The service which creates a dialog and notifiesExpRootComponent
about the change – TheExpRootComponent
component inside the handler for theonModalChange
event the updates the propertymodal
totrue
Once change detection phase is finished, Angular runs a verification loop:
- re-evaluates the host biding expression
modal
inExpRootComponent
which this time produces valuetrue
instead offalse
- compares the two and throws the error because they are different
Let’s see how we can fix this. We have two workarounds to try.
Running local change detection
First, we’ll apply a local change detection approach with detectChanges
like this:
@Component({...})
export class ExpRootComponent {
@HostBinding('class.modal') public modal = false;
constructor(dialogService: DialogService, cdRef: ChangeDetectorRef) {
dialogService.onDialogsChange((dialogs: any) => {
this.modal = dialogs.length > 0;
cdRef.detectChanges();
});
}
}
Interestingly, it doesn’t fix the error. That happens because @HostBinding
properties
are processed as part of checking a parent component,
so calling detectChanges
on the current component won’t work.
We can’t inject and run ApplicationRef.tick()
as this would lead to an infinite loop
and so Angular throws the error “ApplicationRef.tick is called recursively”.
So if we want to use detectChanges
we’ll need to refactor our code
a bit and use the parent component to trigger change detection. Here’s how this could be done:
@Component({
selector: 'app-root',
template: `<exp-root></exp-root>`,
})
export class AppComponent {
constructor(private cdRef: ChangeDetectorRef) {}
detectChanges() {
this.cdRef.detectChanges();
}
}
@Component({...})
export class ExpRootComponent {
@HostBinding('class.modal') public modal = false;
constructor(dialogService: DialogService, parent: AppComponent) {
dialogService.onDialogsChange((dialogs: any) => {
this.modal = dialogs.length > 0;
parent.detectChanges();
});
}
}
As you can see, using detectChanges
sometimes might require a bit of redesign of the application.
Delaying the update
Let’s now try delaying the update with Promise.resolve()
:
@Component({...})
export class ExpRootComponent {
@HostBinding('class.modal') public modal = false;
constructor(dialogService: DialogService) {
dialogService.onDialogsChange((dialogs: any) => {
Promise.resolve().then(() => {
this.modal = dialogs.length > 0;
});
});
}
}
It’s working fine this time:
Architecture-wise, I’d say that it’s the notification code inside DialogDirective
the that should implement delayed update, not the listening code inside ExpRootComponent
.
The code that gets the notification shouldn’t and often can’t know the context of the update,
e.g. it’s coming from inside the change detection cycle.
The code inside the directive that emits the notification knows about the context
and so it’s responsible for delaying the notification.
So here’s how we can refactor the directive that sends the notification:
@Directive({...})
export class DialogDirective {
constructor(private dialogService: DialogService) {}
dialog = null;
@Input('dialog') set open(open: boolean) {
Promise.resolve().then(() => {
if (open) {
this.dialog = this.dialogService.open({});
} else {
this.dialogService.close(this.dialog);
}
});
}
}
This works fine too. And I think it’s a lot better setup when talking about the codebase design.
What’s interesting is that the example we just saw is actually
a simplified version of a setup I had a while ago in the real app.
I used a popular UI library for modals and kept getting the “changed after check” error for a particular scenario.
I spent a couple of hours debugging the error to discover that the error was coming from the library.
This shows that sometimes the “changed after check” error may not be your fault ?.
One final note. Sometimes, an indirect update happens through Output()
event broadcasting, which by default is synchronous.
However, the EventEmitter
implements
a mechanism for a delayed (async) update:
class EventEmitter_ extends Subject<any> {
__isAsync: boolean;
constructor(isAsync: boolean = false) {
super();
this.__isAsync = isAsync;
}
subscribe(observerOrNext?, error?, complete?) {
let nextFn = observerOrNext;
if (this.__isAsync) {
if (nextFn) {
nextFn = _wrapInTimeout(nextFn);
}
}
}
}
In the case of EventEmitter
, just pass true
to the constructor when creating the event bus:
const myEmitter = new EventEmitter(true);
myEmitter.emit('value'); // will emit the value asynchronously