Debugging techniques
In the previous section, we identified several design patterns that commonly lead to
“Expression Changed After It Has Been Checked” error.
In this section we’ll see how to use debugging techniques to identify the leading cause.
The general high-level algorithm to debug the error is the following:
- identify the component, the binding and the expression that yields different values
- single out a component’s property that changes the expression results
- discover how/why the value for this property changes between the regular
detectChanges
run and thecheckNoChanges
verification check
To illustrate how this algorithm can be applied in practice, I’ll use two examples.
The setup from the previous section, where a directive opens a modal dialog via a dedicated service,
is a good example. Another interesting setup involves an OnPush
parent component for which Angular skips the checkNoChanges
check.
So let’s dive in.
A mischievous directive
Inside the template of the ExpDescendant
component we put a logic to display a modal dialog when a button is clicked.
A root component ExpRootComponent
subscribes to notification from The DialogService
and renders the modal into the DOM.
The notifications to show the modal are transmitted through the DialogDirective
.
Here’s the setup again:
@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;
}
The problem with this setup is when we run it the “changed after check” error pops up.
Let’s start by identifying the binding and the component that causes the problem.
One option is to click on the function that ran right before the classProp
directive in the error stack trace:
and it’ll show us the binding and the expression that causes the problem:
Another option is to pause the code execution before Angular throws the error and inspect the callstack.
Put a breakpoint inside throwErrorIfNoChangesMode
function:
Once the execution is paused, click through the callstack to pinpoint the component’s definition and the property
used in the binding expression:
By using any of those methods we discover that the property modal
is what makes the expression yield different values.
Tracking property changes
Now we need to figure how/why the property changes between detectChanges
and checkNoChanges
runs.
One way is to re-define the modal
property of the ExpRootComponent
in runtime as a setter and
log the stacktrace from inside the setter.
But since we can make changes to the sources, we will use the
Proxy object
to intercept all writes to the modal
property:
@Component({...})
export class ExpRootComponent {
@HostBinding('class.modal') public modal = false;
constructor(dialogService: DialogService) {
const self = new Proxy(this, {
set(target, prop, value) {
if (prop === 'modal') {
console.trace(`'modal' prop changed`, value);
}
target[prop] = value;
return true;
}
});
dialogService.onDialogsChange((dialogs: any) => {
self.modal = dialogs.length > 0;
});
return self;
}
}
It is also very convenient to mark the detectChanges
and checkNoChanges
stages because we’re particularly
interested in the modal
property update between those two.
We can do that by adding logpoints to the
tick
method of the ApplicationRef
:
When we click on the button in the application, that’s what we see logged into the console:
This output clearly shows us that there’s an update to the value of the modal
property value between detectChanges
and checkNoChanges
stages.
The stacktrace logged from inside the interceptor can reveal lots of interesting information.
For example, we could simply click on the anonymous function that runs before the setter to show us the line of code
that makes the update:
If we continue going down the stack, we will construct the entire chain of function calls that contributed to the error.
For example, here we can see that directive gets an updated value for the dialog
property as part of checking
the ExpDescendant
component:
From there, the method dialogService.open
is called that triggers a notification to the onDialogsChange
subscription.
All this happens inside the change detection loop which triggers “changed after check” error.
We discussed the proper fixes for this particular application setup
in the previous section.
A curious case of the OnPush component
Let’s take a look at another very interesting setup that I found on StackOverflow.
Assume we know nothing about the code that looks like this:
@Component({
selector: 'parent',
template: `
<div>Data is loaded: {{ dataSize > 0 }}</div>
<button (click)="click()">Load data</button>
<child [data]="data" (stats)="handleStatsChange($event)"></child>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ParentComponent {
data = [];
dataSize: number;
click() {
this.data = ['Data1', 'Data2'];
}
handleStatsChange($event) {
this.dataSize = $event;
}
}
@Component({
selector: 'child',
template: ` <div *ngFor="let item of data">{{ item }}</div> `,
})
export class ChildComponent {
@Input() data;
@Output('stats') statsEmitter = new EventEmitter();
ngOnChanges(changes): void {
let dataSize = changes['data'].currentValue.length;
this.statsEmitter.emit(dataSize);
}
}
By quickly scanning the code we can conclude that the author wants to load some data and display the status on the screen.
When we run this example, we get no error in the console.
But the status still shows false
even after the data is loaded and items are rendered on the screen:
That’s some interesting inconsistency. The question on StackOverflow stated that if they removed the OnPush
on parent,
they would get the “changed after check” error. Once we make that change and run the code again,
indeed the error pops up in the console:
We might assume that the conditions that triggered the error manifested itself as an inconsistency between
the application state and the status rendered on the screen. The error itself just didn’t appear in the console for
some reason until we removed OnPush
from the parent component. Let’s try to figure out why it happens.
By clicking on the ParentComponent_Template
line in the stacktrace, we can discover the expression that yields different values.
This reveals the expression ctx.dataSize > 0
:
The property that we need to track changes for is dataSize
:
@Component({
selector: 'parent',
template: `
<div>Data is loaded: {{dataSize > 0}}</div>
<button (click)="click()">Load data</button>
<child [data]="data" (stats)="handleStatsChange($event)"></child>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ParentComponent {
dataSize: number;
...
}
Let’s see how the value for this property changes between the detectChanges
and checkNoChanges
.
Tracking property changes
The dataSize
property is updated inside the handleStatsChange
method of the ParentComponent
.
This method runs when the child
component emits stats
event:
@Component({
selector: 'parent',
template: `
<div>Data is loaded: {{dataSize > 0}}</div>
<button (click)="click()">Load data</button>
<child [data]="data" (stats)="handleStatsChange($event)"></child>
`,
})
export class ParentComponent {
dataSize: number;
handleStatsChange($event) {
this.dataSize = $event;
}
}
@Component({...})
export class ChildComponent {
@Input() data;
@Output('stats') statsEmitter = new EventEmitter();
ngOnChanges(changes) {
let dataSize = changes['data'].currentValue.length;
this.statsEmitter.emit(dataSize);
}
}
Since the stats
event is emitted synchronously from inside ngOnChanges
which runs as part of change detection,
it’s expected that we’ll get the error.
An interesting question is why there’s no error if the parent component defined as OnPush
.
Let’s add a logpoint to the child
component’s template to see how many times Angular
runs the update logic for the textInterpolate
binding:
We also have logpoints inside the tick
method to mark a change detection stage.
When we run the application and check the console, we’ll discover that Angular runs the template function
just once for detectChanges
, not twice:
That means that checkNoChanges
stage is skipped. We can assume that this happens because since the component is OnPush
,
it’s marked as dirty on click and checked during detectChanges
phase. But part of the check logic
resets the component’s state
to pristine (non-dirty) after it’s been checked:
export function refreshView(tView, lView, templateFn, context) {
...
if (!isInCheckNoChangesPass) {
lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);
}
}
this leads to skipping the checkNoChanges
for the ParentComponent
.
We can see it here:
and because of this reset Angular doesn’t check ParentComponent
during the subsequent checkOnChanges
phase:
If we remove the OnPush
declaration, the update logic runs twice as expected and the checkNoChanges
phase produces the error:
Note that the type of binding doens’t have any effect on the situation. If we used ngIf
instead of the
text interpolation to render the status text:
@Component({
selector: 'parent',
template: `
<span *ngIf="dataSize > 0">Data is loaded</span>v>
<button (click)="click()">Load data</button>
<child [data]="data" (stats)="handleStatsChange($event)"></child>
`,
})
we would get the same error:
This time however a different binding would throw the error.
As you can see in the stacktrace that would be property
binding that updates a value for ngIf
expression.
The fix
So the fact that the error is not there when OnPush
is used on the parent component is misleading and removing OnPush
isn’t a proper fix.
Regardless whether the error is thrown or not, the application state and UI remain inconsistent –
the status text shows false
, but it should show true
. This situation is exactly what the “changed after check”
error is supposed to detect and alert users about, but with an OnPush
component this erroneous situation goes unnoticed.
So what would be the proper fix? We could use detectChanges
inside the handler like this:
@Component({...})
export class ParentComponent {
data = [];
dataSize: number;
constructor(private cdRef: ChangeDetectorRef) {}
click() {
this.data = ['Data1', 'Data2'];
}
handleStatsChange($event) {
this.dataSize = $event;
this.cdRef.detectChanges();
}
}
which will result in the proper update of the status and the following log output:
Before the child component is checked, the value for dataSize
is 0
. Once Angular checks the child component, the value is updated through the event emission. Once we run detectChanges
manually from inside the handler, Angular updates takes the value 2
for dataSize
and correctly renders the status on the screen. This workaround works and fixes the state incconsistentency.
Another approach is to use asynchronous update. Here’s how we can use the resolved promise approach:
@Component({...})
export class ChildComponent {
@Input() data;
@Output('stats') statsEmitter = new EventEmitter();
ngOnChanges(changes): void {
let dataSize = changes['data'].currentValue.length;
Promise.resolve().then(() => {
this.statsEmitter.emit(dataSize);
});
}
}
which will result in the proper update of the status and the following log output:
Here you can see two change detection cycles running one after the other.
The second one is scheduled when we use Promise.resolve
.
Instead of the resolved promise approach, we could achieve a delayed update through the async event like this:
@Component({...})
export class ChildComponent {
@Input() data;
@Output('stats') statsEmitter = new EventEmitter(true);
ngOnChanges(changes): void {
let dataSize = changes['data'].currentValue.length;
this.statsEmitter.emit(dataSize);
}
}
This will use setTimeout
instead of the resolved promise.