[英]Updating value in parent component from child one causes ExpressionChangedAfterItHasBeenCheckedError in Angular
I have two component: ParentComponent > ChildComponent
and a service, eg TitleService
. 我有两个组件: ParentComponent > ChildComponent
和一个服务,例如TitleService
。
ParentComponent
looks like this: ParentComponent
看起来像这样:
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: ChildComponent
看起来像这样:
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. 这个想法非常简单: ParentController
在屏幕上显示标题。 In order to always render the actual title it subscribes to the TitleService
and listens for events. 为了始终呈现实际标题,它订阅TitleService
并侦听事件。 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. 当ChildComponent
加载时,它从路由器获取数据(动态解析)并告诉TitleService
使用新值更新标题。
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. 我可以将setTitle()
和onTitleChange()
调用移动到受尊重的构造函数,但我已经读过,除了初始化本地属性之外,在构造函数逻辑中执行任何“繁重的工作”被认为是一种不好的做法。
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 . 您可以在GitHub存储库中找到它。
After thorough investigation the problem only occurred when using *ngIf="title"
in ParentComponent
: 彻底调查后,只有在ParentComponent
使用*ngIf="title"
时才会出现问题:
<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. 文章关于ExpressionChangedAfterItHasBeenCheckedError
错误需要了解的所有内容都会详细解释此行为。
There are two possible solutions to your problem: 您的问题有两种可能的解决方案:
1) put app-child
before ngIf
: 1)在ngIf
之前放置app-child
:
<app-child></app-child>
<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>
2) Use asynchronous event: 2)使用异步事件:
export class TitleService {
private titleChangeEmitter = new EventEmitter<string>(true);
^^^^^^--------------
After thorough investigation the problem only occurred when using *ngIf="title" 彻底调查后,问题只发生在使用* 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 : 您描述的问题并非特定于ngIf
,可以通过实现自定义指令轻松复制,该指令依赖于在输入传递给指令后在更改检测期间同步更新的父输入:
@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. 为什么发生这种情况实际上需要很好地理解特定于更改检测和组件/指令表示的Angular内部。 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. 在摘要周期中, Angular对子指令执行某些操作。 One of such operations is updating inputs and calling ngOnInit
lifecycle hook on child directives/components. 其中一个操作是更新输入并在子指令/组件上调用ngOnInit
生命周期钩子。 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. 在执行上述操作时,Angular遵循此层次结构。 So, assume currently Angular checks parent-component
: 因此,假设当前Angular检查parent-component
:
title
input binding on ng-if
, set it to initial value undefined
更新ng-if
上的title
输入绑定,将其设置为初始值undefined
ngOnInit
for ng-if
. 为ng-if
调用ngOnInit
。 child-component
child-component
不需要更新 ngOnInti
for child-component
which changes title to Dynamic Title
on parent-component
为child-component
调用ngOnInti
,在parent-component
child-component
上更改Dynamic Title
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
. 因此,我们最终得到的结果是Angular在更新ng-if
上的属性时传递了title=undefined
,但是当更改检测完成时,我们在parent-component
上有title=Dynamic Title
。 Now, Angular runs second digest to verify there's no changes. 现在,Angular运行第二个摘要以验证没有变化。 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. 但是当它与传递给ng-if
内容相比时,它使用当前值来检测更改并抛出错误。
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
. 更改parent-component
模板中ng-if
和child-component
的顺序将导致parent-component
属性在ng-if
角度更新属性之前更新ng-if
。
You could try using the ChangeDetectorRef
so Angular will be manually notified about the change and the error won't be thrown. 您可以尝试使用ChangeDetectorRef
以便手动通知Angular有关更改,并且不会抛出错误。
After changing title
in ParentComponent
call the detectChanges
method of the ChangeDetectorRef
. 改变后title
在ParentComponent
调用detectChanges
的方法ChangeDetectorRef
。
export class ParentComponent implements OnInit, OnDestroy {
title: string;
private titleSubscription: Subscription;
constructor(private titleService: TitleService, private changeDetector: ChangeDetectorRef) {
}
public ngOnInit() {
this.titleSubscription = this.titleService.onTitleChange()
.subscribe(title => this.title = title);
this.changeDetector.detectChanges();
}
public ngOnDestroy(): void {
if (this.titleSubscription
) {
this.titleSubscription.unsubscribe();
}
}
}
In my case, I was changing the state of my data --this answer requires you to read AngularInDepth.com explanation of digest cycle-- inside the html level , all I had to do was change the way I was handling the data like so: 在我的情况下,我正在改变我的数据状态 - 这个答案要求你阅读AngularInDepth.com的摘要周期解释 - 在html级别内 ,我所要做的就是改变我处理数据的方式,如此:
<div>{{event.subjects.pop()}}</div>
into 成
<div>{{event.subjects[0]}}</div>
Summary: instead of popping--which returns and remove the last element of an array thus changing the state of my data-- I used the normal way of getting my data without changing its state thus preventing the exception. 总结:而不是弹出 - 它返回并删除数组的最后一个元素,从而改变我的数据状态 - 我使用正常的方式获取我的数据而不改变其状态,从而防止异常。
A 'quick' solution in some cases is to use setTimeout()
. 在某些情况下,“快速”解决方案是使用setTimeout()
。 You need to consider all the caveats (see the articles other people have referred to) but for a simple case like just setting a title this is the easiest way. 你需要考虑所有的警告(参见其他人提到过的文章)但是对于一个简单的案例,比如设置一个标题,这是最简单的方法。
ngOnInit (): void {
// Watching for title change.
this.titleSubscription = this.titleService.onTitleChange().subscribe(title => {
setTimeout(() => { this.title = title; }, 0);
});
}
Use this as a last resort type solution if you're not yet comfortable understanding all the complexities of change detection. 如果您还不能理解变更检测的所有复杂性,请将此作为最后的解决方案。 Then come back and fix it another way when you are :) 然后回来,当你是:)时,另一种方式修复它:)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.