Angularで発生する「ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'」エラーの解説
Angularアプリケーション開発において、ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'
エラーが発生する可能性があります。このエラーは、テンプレート内でバインドされた式の値が、変更検知後に変化してしまうことが原因で発生します。
原因
このエラーが発生する主な原因は以下の2つです。
- テンプレート内で直接変数を変更する
<input type="text" [(ngModel)]="name">
<button (click)="name = 'John Doe'">名前を変更</button>
上記のコード例では、name
変数を直接変更しているため、変更検知後にテンプレートが更新されずエラーが発生します。
- 非同期処理内で変数を変更する
<input type="text" [(ngModel)]="name">
<button (click)="getData()">データを取得</button>
getData() {
this.http.get('...').subscribe(data => {
this.name = data.name; // 非同期処理内で変数を変更
});
}
解決方法
このエラーを解決するには、以下の方法があります。
<input type="text" [(ngModel)]="name">
<button (click)="changeName()">名前を変更</button>
changeName() {
this.name = 'John Doe'; // コンポーネント内で変数を変更
}
上記のコード例のように、テンプレート内で直接変数を変更せず、コンポーネント内で変数を変更することでエラーを回避できます。
- 非同期処理内でasyncパイプを使用する
<input type="text" [(ngModel)]="name | async">
<button (click)="getData()">データを取得</button>
getData() {
this.name = this.http.get('...').pipe(
map(data => data.name)
);
}
上記のコード例のように、非同期処理内でasync
パイプを使用することで、変更検知後にテンプレートが自動的に更新されます。
上記以外にも、このエラーが発生する原因は考えられます。詳細な原因特定には、コード全体を確認する必要があります。
補足
- 上記のコード例は、あくまで参考例です。実際のコードは、状況に合わせて変更する必要があります。
- 日本語での解説は、より分かりやすくするために一部省略している場合があります。
app.component.html
<h1>ExpressionChangedAfterItHasBeenCheckedError</h1>
<p>名前:{{ name }}</p>
<input type="text" [(ngModel)]="name">
<button (click)="changeName()">名前を変更</button>
<button (click)="getData()">データを取得</button>
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
})
export class AppComponent {
name: string = 'John Doe';
constructor(private http: HttpClient) {}
changeName() {
// テンプレート内で直接変数を変更
this.name = 'Jane Doe';
}
getData() {
// 非同期処理内で変数を変更
this.http.get('...').subscribe(data => {
this.name = data.name;
});
}
}
エラー発生例
- 上記コードを
ng serve
コマンドで実行します。 - テンプレート内の入力欄に値を入力し、Enterキーを押します。
- "名前を変更"ボタンをクリックします。
上記の手順を実行すると、コンソールに以下のエラーメッセージが表示されます。
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'John Doe'.
以下の方法でコードを変更することで、エラーを回避できます。
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
})
export class AppComponent {
name: string = 'John Doe';
constructor(private http: HttpClient) {}
changeName() {
// コンポーネント内で変数を変更
this.name = 'Jane Doe';
}
getData() {
// 非同期処理内で変数を変更
this.http.get('...').subscribe(data => {
this.name = data.name;
});
}
}
<h1>ExpressionChangedAfterItHasBeenCheckedError</h1>
<p>名前:{{ name | async }}</p>
<input type="text" [(ngModel)]="name">
<button (click)="changeName()">名前を変更</button>
<button (click)="getData()">データを取得</button>
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
})
export class AppComponent {
name: string = 'John Doe';
constructor(private http: HttpClient) {}
changeName() {
// コンポーネント内で変数を変更
this.name = 'Jane Doe';
}
getData(): Observable<string> {
// 非同期処理内で`async`パイプを使用
return this.http.get('...').pipe(
map(data => data.name)
);
}
}
確認方法
上記の修正コードを実行して、エラーが発生しないことを確認してください。
ExpressionChangedAfterItHasBeenCheckedError エラーの解決方法
<input type="text" [(ngModel)]="name">
<button (click)="changeName()">名前を変更</button>
changeName() {
// テンプレート内で直接変数を変更
this.name = 'John Doe';
}
解決策
<input type="text" [(ngModel)]="name">
<button (click)="changeName()">名前を変更</button>
changeName() {
// コンポーネント内で変数を変更
this.name = 'John Doe';
this.cdr.detectChanges(); // 変更検知を強制的に実行
}
constructor(private cdr: ChangeDetectorRef) {}
<input type="text" [(ngModel)]="name | async">
<button (click)="getData()">データを取得</button>
getData() {
this.http.get('...').subscribe(data => {
this.name = data.name; // 非同期処理内で変数を変更
});
}
<input type="text" [(ngModel)]="name | async">
<button (click)="getData()">データを取得</button>
getData() {
this.name = this.http.get('...').pipe(
map(data => data.name)
);
}
setTimeout を使用する
<input type="text" [(ngModel)]="name">
<button (click)="getData()">データを取得</button>
getData() {
this.http.get('...').subscribe(data => {
setTimeout(() => {
this.name = data.name; // setTimeout を使用して、次の変更検知サイクルで変数を変更
}, 0);
});
}
上記のコード例では、setTimeout
を使用して、次の変更検知サイクルで変数を変更することで、エラーを回避できます。
ChangeDetectorRef を使用する
<input type="text" [(ngModel)]="name">
<button (click)="getData()">データを取得</button>
getData() {
this.http.get('...').subscribe(data => {
this.name = data.name;
this.cdr.detectChanges(); // 変更検知を強制的に実行
});
}
constructor(private cdr: ChangeDetectorRef) {}
上記のコード例では、ChangeDetectorRef
を使用して、変更検知を強制的に実行することで、エラーを回避できます。
markForCheck を使用する
<input type="text" [(ngModel)]="name">
<button (click)="getData()">データを取得</button>
getData() {
this.http.get('...').subscribe(data => {
this.name = data.name;
this.cdr.markForCheck(); // コンポーネントを再チェック対象としてマーク
});
}
constructor(private cdr: ChangeDetectorRef) {}
上記のコード例では、markForCheck
を使用して、コンポーネントを再チェック対象としてマークすることで、エラーを回避できます。
NgZone を使用する
<input type="text" [(ngModel)]="name">
<button (click)="getData()">データを取得</button>
getData() {
this.ngZone.run(() => {
angular runtime-error