I have two component: ParentComponent > ChildComponent
and a service, e.g. TitleService
.
ParentComponent
looks like this:
export class ParentComponent implements OnInit, OnDestroy {
title: string;
private titleSubscription: Subscription;
constructor (private titleService: TitleService) {
}
ngOnInit (): void {
// Watching for title change.
this.titleSubscription = this.titleService.onTitleChange()
.subscribe(title => this.title = title)
;
}
ngOnDestroy (): void {
if (this.titleSubscription) {
this.titleSubscription.unsubscribe();
}
}
}
ChildComponent
looks like this:
export class ChildComponent implements OnInit {
constructor (
private route: ActivatedRoute,
private titleService: TitleService
) {
}
ngOnInit (): void {
// Updating title.
this.titleService.setTitle(this.route.snapshot.data.title);
}
}
The idea is very simple: ParentController
displays the title on screen. In order to always render the actual title it subscribes to the TitleService
and listens for events. When title is changed, the event happens and title is updated.
When ChildComponent
loads, it gets data from the router (which is resolved dynamically) and tells TitleService
to update the title with the new value.
The problem is this solution causes this error:
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'Dynamic Title'.
It looks like the value is updated in a change detection round.
Do I need to re-arrange the code to have a better implementation or do I have to initiate another change detection round somewhere?
I can move the setTitle()
and onTitleChange()
calls to the respected constructors, but I've read that it's considered a bad practice to do any "heavy-lifting" in the constructor logic, besides initializing local properties.
Also, the title should be determined by the child component, so this logic couldn't be extracted from it.
I've implemented a minimal example to better demonstrate the issue. You can find it in the GitHub repository.
After thorough investigation the problem only occurred when using *ngIf="title"
in ParentComponent
:
<p>Parent Component</p>
<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>
<hr>
<app-child></app-child>
The article Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError
error explains this behavior in great details.
There are two possible solutions to your problem:
1) put app-child
before ngIf
:
<app-child></app-child>
<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>
2) Use asynchronous event:
export class TitleService {
private titleChangeEmitter = new EventEmitter<string>(true);
^^^^^^--------------
After thorough investigation the problem only occurred when using *ngIf="title"
The problem you're describing is not specific to ngIf
and can be easily reproduced by implementing a custom directive that depends on parent input that is synchronously updated during change detection after that input was passed down to a directive:
@Directive({
selector: '[aDir]'
})
export class ADirective {
@Input() aDir;
------------
<div [aDir]="title"></div>
<app-child></app-child> <----------- will throw ExpressionChangedAfterItHasBeenCheckedError
Why that happens actually requires a good understanding of Angular internals specific to change detection and component/directive representation. You can start with these articles:
Although it's not possible to explain everything in details in this answer, here is the high level explanation. During digest cycle Angular performs certain operations on child directives. One of such operations is updating inputs and calling ngOnInit
lifecycle hook on child directives/components. What's important is that these operations are performed in strict order:
Now you have the following hierarchy:
parent-component
ng-if
child-component
And Angular follows this hierarchy when performing above operations. So, assume currently Angular checks parent-component
:
title
input binding on ng-if
, set it to initial value undefined
ngOnInit
for ng-if
.child-component
ngOnInti
for child-component
which changes title to Dynamic Title
on parent-component
So, we end up with a situation where Angular passed down title=undefined
when updating properties on ng-if
but when change detection is finished we have title=Dynamic Title
on parent-component
. Now, Angular runs second digest to verify there's no changes. But when it compares to what was passed down to ng-if
on the previous digest with the current value it detects a change and throws an error.
Changing the order of ng-if
and a child-component
in the parent-component
template will lead to the situation when property on parent-component
will be updated before angular updates properties for a ng-if
.