ExpressionChangedAfterItHasBeenCheckedErrorについて


ExpressionChangedAfterItHasBeenCheckedErrorとは

子コンポーネントに値を渡した後に親コンポーネントの値が変わってたときに出てくるエラーみたいです。

エラーになる原因

Angular2の変更検知(change detection)

Angular2の変更検知オペレーションを理解しないとエラーになる原因がすっかり理解できないため、その処理を簡単にまとめてみます。

変更検知処理

チェックプロセス1
- 子コンポーネント・ディレクティブにバウンディングしているプロパティを更新する
- 子コンポーネントのngOnInit, onChanges, ngDoCheckをinvokeする
- 現在のDOMを更新する
- 子コンポーネントの変更検知処理(チェックプロセス1再帰)を実行する
- 子コンポーネントのngAfterViewInitをinvokeする

引用元:Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error

各ステップの各プロパティはコンポーネントのoldValuesプロパティに保存しています。上記の処理が終わったら、下記の処理(digest cycle)を行い、oldValuesに保存された値と現在コンポーネントの値の比較が行われます:

チェックプロセス2
- oldValuesに保存された子コンポーネント用プロパティと現在コンポーネントもつ値が一致かどうかをチェック
- oldValuesに保存された現在のDOMを更新プロパティと現在コンポーネント持つ値が一致かどうかをチェック
- 子コンポーネントにチェックプロセス2を実行する

で、下記の例でエラーになる原因を説明させていただきます。
例えば、コンポーネントAと子コンポーネントBがあります。
コンポーネントAが子コンポーネントBに"text"プロパティを渡したい場合は:
1. コンポーネントAのチェックプロセス1を実行する(textプロパティをoldValuesに記録する)
2. コンポーネントBのチェックプロセス1を実行する(ここでおやコンポーネントAのtextプロパティが変った、理由は後ほど)
3. コンポーネントAのチェックプロセス2を実行する
4. oldValuesに記録されたtextプロパティと現在コンポーネントA持つtextプロパティが不一致のため、処理が中止され、ExpressionChangedAfterItHasBeenCheckedErrorがスローされます。

プロパティが変った原因

色々なケースがありますが、下記のようにコンポーネントBのngOnInit()で、おやコンポーネントAのtextプロパティを強制的に変更する例があります。:

export class BComponent {
    @Input() text;

    constructor(private parent: AppComponent) {}

    ngOnInit() {
        this.parent.text = 'updated text';
    }
}

引用元:Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error

解決策

export class BComponent {
    @Input() text;

    constructor(private parent: AppComponent) {}

    ngOnInit() {
        // setTimeoutで、コンポーネントAのチェックプロセス2を先に実行させてから、おやコンポーネントAのプロパティ値を変更してもエラーにならない(チェック処理後値を変更するからだ)
        setTimeout(this.parent.text = 'updated text', 1000);
    }
}

まとめ

  • Angular2の変更検知処理がこのエラーになる原因です。
  • 子コンポーネントのライフサイクル・フックでおやコンポーネントのプロパティを変更するのは危ないです。