ExpressionChangedAfterItHasBeenCheckedErrorとは?発生原因と解決方法を解説!

この記事では、Angularの開発中によく遭遇するエラーの一つである『ExpressionChangedAfterItHasBeenCheckedError』について、以下の内容をサンプルコードを用いてわかりやすく解説します。

  • ExpressionChangedAfterItHasBeenCheckedErrorとは
  • ExpressionChangedAfterItHasBeenCheckedErrorの解決方法

ExpressionChangedAfterItHasBeenCheckedErrorとは

Angularの変更検知(Change Detection)中にコンポーネントのデータバインディングが予期せず変更されると、Angularは「既にチェック済みの値が変更された」と認識し、ExpressionChangedAfterItHasBeenCheckedErrorが発生します。これは、Angularの開発中によく遭遇するエラーの一つです。

Angularはビュー(画面)を更新するために「変更検知」という作業を行います。変更検知の開始時に、Angularはコンポーネントのデータバインディングをチェックし、ビューが最新の状態であることを確認します。変更検知の終了時にも、再びコンポーネントのデータバインディングの値が変更されていないかをチェックします。この最終チェックの段階で、既にチェック済みの値が変更されていると、ExpressionChangedAfterItHasBeenCheckedErrorが発生します。

例えば、以下のような場合でExpressionChangedAfterItHasBeenCheckedErrorが発生します。

  • ライフサイクルフック内での値変更
    • ngAfterViewInitなどのライフサイクルフック内でプロパティの値を変更した場合。
  • 親から子コンポーネントに渡した値の変更
    • 親コンポーネントが子コンポーネントに値を渡した後、その値がさらに変更された場合。
  • 非同期操作
    • タイマーやAPI呼び出しの結果、データが更新された場合。

各場合においてExpressionChangedAfterItHasBeenCheckedErrorを発生させてみましょう。

補足

変更検知の終了時にコンポーネントのデータバインディングの値が変更されていないかをチェックするのは、開発モードのみです。そのため、ExpressionChangedAfterItHasBeenCheckedErrorは開発モードでのみ発生します。

開発モードでは、アプリケーションが正しく動作しているかを検証するために、各変更検知の終了後に追加のチェックを実行してコンポーネントのデータバインディングが変更されていないことを確認しています。

ライフサイクルフック内での値変更

ExpressionChangedAfterItHasBeenCheckedErrorを発生させるサンプルコードを以下に示します。

import { Component, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <div>
      <p>カウント: {{ count }}</p>
      <button (click)="increment()">Increment</button>
    </div>
  `
})
export class AppComponent implements AfterViewInit {
  count = 0;

  ngAfterViewInit(): void {
    // AfterViewInitの後に値を変更すると、ExpressionChangedAfterItHasBeenCheckedErrorが発生
    this.count = 1;
  }

  increment(): void {
    this.count++;
  }
}

ngAfterViewInitはビューが描画された直後に何かしらの処理を行うためのライフサイクルフックです。すなわち、ngAfterViewInit内の処理が実行される前には、Angularはすでにコンポーネントのデータバインディングの値をチェックし、コンポーネントの状態をレンダリングしています。

そのため、ngAfterViewInit 内でコンポーネントのデータバインディングの値を変更すると、Angularは「既にチェック済みの値が変更された」と認識し、ExpressionChangedAfterItHasBeenCheckedError が発生します。

ExpressionChangedAfterItHasBeenCheckedErrorの解決方法

ExpressionChangedAfterItHasBeenCheckedErrorを防ぐ解決策としては、以下の方法があります。

  • Angularのライフサイクルチェック外で値を変更する
  • ChangeDetectorRefを使用して手動で変更検知を実行する

Angularのライフサイクルチェック外で値を変更する

例えば、setTimeoutを使用すると、Angularのライフサイクルフック(例えば、ngOnInitngAfterViewCheckedなど)の処理が終わった後に値を変更することができるので、エラーを回避することができます。サンプルコードを以下に示します。

ngAfterViewInit(): void {
  setTimeout(() => {
    this.count = 1;
  });
}

ChangeDetectorRefを使用して手動で変更検知を実行する

ChangeDetectorRefを使用すると、Angularに手動で状態変更を認識させることができ、エラーを回避することができます。サンプルコードを以下に示します。

import { AfterViewInit, ChangeDetectorRef, Component } from '@angular/core';
@Component({
  selector: 'app-root',
  template: `
    <div>
      <p>カウント: {{ count }}</p>
      <button (click)="increment()">Increment</button>
    </div>
  `,
})
export class AppComponent implements AfterViewInit {
  constructor(private cdr: ChangeDetectorRef) {}
  count = 0;

  ngAfterViewInit(): void {
    this.count = 1;
    this.cdr.detectChanges();
  }

  increment(): void {
    this.count++;
  }
}

親コンポーネントが子コンポーネントに値を渡した後にその値が変更された場合

ExpressionChangedAfterItHasBeenCheckedErrorを発生させるサンプルコードを以下に示します。

親コンポーネント (parent.component.ts)

import { Component } from '@angular/core';

@Component({
  selector: 'app-parent',
  template: `
    <div>
      <h2>Parent Component</h2>
      <button (click)="updateValue()">Update Value</button>
      <app-child [value]="parentValue"></app-child>
    </div>
  `,
})
export class ParentComponent {
  parentValue = 'Initial Value';

  updateValue() {
    this.parentValue = 'Updated Value';
    setTimeout(() => {
      this.parentValue = 'Updated Again After Change Detection';
    }, 0);
  }
}

子コンポーネント (child.component.ts)

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-child',
  template: `
    <div>
      <h3>Child Component</h3>
      <p>Value: {{ value }}</p>
    </div>
  `,
})
export class ChildComponent {
  @Input() value!: string;
}

親コンポーネントにはparentValueというプロパティがあり、子コンポーネントに@Input()デコレーターを使用して渡しています。updateValue()メソッドはボタンのクリックによって呼び出され、parentValueを更新します。変更検出の終了後にsetTimeout()を使って、parentValueを変更しているため、ExpressionChangedAfterItHasBeenCheckedErrorが発生します。

本記事のまとめ

この記事ではAngularの開発中によく遭遇するエラーの一つである『ExpressionChangedAfterItHasBeenCheckedError』について、以下の内容を説明しました。

  • ExpressionChangedAfterItHasBeenCheckedErrorとは
  • ExpressionChangedAfterItHasBeenCheckedErrorの解決方法

お読み頂きありがとうございました。