ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'something: null'. Current value: 'something: something'.
Componentを入れ子構造にしているとよく見るこのエラー
どうも、子供のComponentから親Componentを変更しようとすると発生するようだ。
Angularでは、子供のComponentからの親の変更を基本的に禁止しているので、こういうことが出るらしい。
とりあえずこの2つのどっちかでOK
- setTimeout による非同期処理化
- detectChanged による明示的な変更
今回の例の前提と登場人物
子Componentにより、親ComponentのViewにあるタイトルが変更されるとする。
- 親 Component (ParentComponent)
- 子 Component (ChildComponent)
- タイトルを司るサービス(TitleService)
ParentComponent
Typescript
@Component({
selector: 'app-parent',
templateUrl: 'app-parent.component.html',
})
export class ParentComponent implements OnInit {
title: string;
constructor(
private titleService: TitleService,
) {}
ngOnInit(): void {
this.titleService.value.subscribe((value) => {
this.title = value;
});
}
}
View
<div>
<h1>{{ title }} </h1>
<ng-content></ng-content>
</div>
ChildComponent
@Component({
selector: 'app-child',
templateUrl: 'app-child.component.html',
})
export class ChildComponent implements OnInit {
constructor(
private titleService: TitleService,
) {}
ngOnInit(): void {
this.titleService.value.next('子供');
}
}
TitleService
@Injectable()
export class TitleService {
value: BehaviorSubject<string> = new BehaviorSubject(null);
}
上記コードを実行すると、
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'value: null'. Current value: 'value: 子供'.
というエラーが出る。
setTimeout による非同期化
これは非常に簡単。でも、なんで setTimeout
するんだ?という一見謎のコードになる。
@Component({
selector: 'app-parent',
templateUrl: 'app-parent.component.html',
})
export class ParentComponent implements OnInit {
title: string;
constructor(
private titleService: TitleService,
) {}
ngOnInit(): void {
this.titleService.value.subscribe((value) => {
setTimeout(() => {
this.title = value;
});
});
}
}
不自然なコードになり、メンテナンス性が下がるので、個人的には下記をおすすめする
detectChanged による明示的な変更
@Component({
selector: 'app-parent',
templateUrl: 'app-parent.component.html',
})
export class ParentComponent implements OnInit {
title: string;
constructor(
private titleService: TitleService,
private cd: ChangeDetectorRef,
) {}
ngOnInit(): void {
this.titleService.value.subscribe((value) => {
this.title = value;
this.cd.detectChanges();
});
}
}
DIするサービスが一つ増えるので、面倒になるのだが、こちらは何をやっているかが明確(なぜやっているかは明確ではないけど)
最後に
どちらの場合でも、エラーは消えるはずです。複数人でメンテし続けるなら、後者が良いかなと思いますが、速さ重視の個人プロダクトなら、前者もありかと思います。
この内容 https://qiita.com/seteen/items/16246f6351c1c4cb281b でも紹介した内容でしたが、今回は解決策を増やして、コードを少し充実させました。
参考