はじめに
Angularでの開発中、ngIfでExpressionChangedAfterItHasBeenCheckedErrorが発生する現象に遭遇しました。
こちらの原因と暫定的対応案に関しての記事になります。
発生時の状況
- 子コンポーネントの処理で、@Output()で親コンポーネントにEventEmitterで値Aを送信している
- 親コンポーネントの処理で、上記で送信された値Aを元にローカル変数A’を書き換えている
- 親コンポーネントのHTMLテンプレートで、ngIfにA’を利用しており、その後に子コンポーネントを呼び出している
再現用サンプルプログラム
import { Component, Input } from '@angular/core'; @Component({ selector: 'parent', template: ` <span *ngIf="isDisplay">parent</span> <child (emitter)=changeMessage($event)></child> ` }) export class ParentComponent { // 値A' isDisplay: boolean; changeMessage($event: boolean) { this.isDisplay = $event; } }
import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'child', template: `` }) export class ChildComponent { @Output() emitter = new EventEmitter<boolean>(); // 値A isDisplay: boolean; ngOnInit() { this.emitter.emit(true); } }
処理の流れ・原因
サンプルプログラムの処理の流れは以下のようになります。
- ngIfにバインドされているisDisplayの初期値にundefinedを設定
- ngIfのngOnInitを呼び出し→ngIfの表示内容を確認
- child.component.tsのngOnInitを呼び出し→isDisplayの値がtrueに更新
- 2の確認後に3の処理でngIfの内容が変更されているためにExpressionChangedAfterItHasBeenCheckedErrorが発生
暫定的回避法
以下のように子コンポーネントを修正することでエラー発生を防げます。
// @Output() emitter = new EventEmitter<boolean>(); // ↓ @Output() emitter = new EventEmitter<boolean>(true);
EventEmitterのコンストラクタにtrueを指定することでemit処理を非同期化できます。
非同期化することにより変更検知処理が新たに別で走るようになるため、ExpressionChangedAfterItHasBeenCheckedErrorは発生しません。
注意点
そもそもこのような状況を作り出さないように、データが親→子で一方向になるようにPG設計をすることが理想的です。今回の方法では、変更検知過多による負荷上昇・変更検知の無限ループなどを引き起こす可能性も考慮する必要があるためです。あくまでも暫定的な回避法として参考にしていただければと思います。