- Angular
ExpressionChangedAfterItHasBeenCheckedの直し方
ExpressionChangedAfterItHasBeenCheckedError(NG0100)は、「画面を描いたあとで、テンプレートに出している値が変わった」ことを Angular が見つけたときのエラーです。 開発モードでは、変更検知(画面の値を最新にする処理)のあとに、もう一度だけ値を読み直して比べます。そこで値が違うと、このエラーになります。
ERROR RuntimeError: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '読み込み中'. Current value: '完了'. Expression location: _EagerAvi component. Find more at https://v22.angular.dev/errors/NG0100
直し方は、「描く前に値を決める」か「値を Signal にして、変わったことを Angular に伝える」かのどちらかです。 setTimeout や detectChanges() で消す方法もありますが、v22 の zoneless(Zone.js を使わない、v21 からの既定)では setTimeout だと画面が更新されませんでした。
| 原因 | 直し方の第一候補 |
|---|---|
ngAfterViewInit でテンプレートの値を変える |
値を Signal にする/ngOnInit か初期値で決める |
| 子コンポーネントが、親の表示中の値を変える | 親の値を Signal にする/状態を親かサービスに置く |
| getter・メソッドが呼ぶたびに違う値を返す | 値をフィールドか computed() に持つ |
afterNextRender の中で普通のフィールドを変える |
変える値を Signal にする |
setTimeout の中で普通のフィールドを変える |
Signal にする(zoneless では画面が更新されない) |
確認環境:Angular 22.2.2(@angular/core・@angular/cli)、TypeScript 6.0.3、Node.js 24.15.0(Linux)。ng new で作ったプロジェクトで、TestBed+Vitest 5.0.4(jsdom)と、ng build した画面を Chromium(Playwright 1.56.1)で開いて再現しました。エラー文は実際に出たものです(確認日 2026年10月12日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「ExpressionChangedAfterItHasBeenCheckedの直し方」
エラー文の読み方
v22.2.2 で出たエラー文は、次の形でした。
NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'title': 'a'. Current value: 'b'. Expression location: _PropBinding component. Find more at https://v22.angular.dev/errors/NG0100
| 部分 | 意味 |
|---|---|
Previous value: '…' |
1回目の変更検知で画面に出した値 |
Current value: '…' |
確認のために読み直したときの値 |
for 'title' |
プロパティバインディング([title]="…")のとき、その名前。{{ }} のときは出ない |
Expression location: _PropBinding component |
その式が書かれているコンポーネント。先頭の _ は、開発ビルドでクラス名に付いたもの |
探すときは、Expression location のコンポーネントのテンプレートで、Previous value の値を出している式を見ます。 その値を「描いたあと」に変えている処理が原因です。
なぜ開発モードでだけ出るのか
v22.2.2 の ApplicationRef.tick()(変更検知の入口)は、次の順で動いていました(@angular/core の配布コードで確認)。
- 変更検知をして、画面の値を最新にする
- 開発モード(
ngDevModeが有効)のときだけ、全部のビューでcheckNoChanges()を呼び、式をもう一度読んで前の値と比べる - 違えば NG0100 を投げる
本番ビルド(ng build の既定)では 2 が動きません。そのため、同じコードを本番ビルドで開くとエラーは出ませんでした。ただし、画面の表示は開発ビルドと同じく「読み込み中」のままでした。エラーが出ないだけで、画面に前の値が残る不具合はそのままです。
| ビルド | コンソール | 画面の表示 |
|---|---|---|
ng build --configuration development |
ERROR RuntimeError: NG0100: … |
読み込み中 |
ng build(本番) |
なし | 読み込み中 |
(どちらも、後で説明する provideCheckNoChangesConfig({ exhaustive: true }) を入れ、ngAfterViewInit で値を変える同じコンポーネントを開いた結果です)
v22 では「エラーが出ないまま表示が更新されない」ことがある
Angular v22 では、コンポーネントの変更検知の既定が OnPush になりました。 angular.dev の ChangeDetectionStrategy の説明にも「OnPush is enabled by default.」とあり、これまでの既定の動き(毎回チェックする)は ChangeDetectionStrategy.Eager という名前になっています(Default は非推奨の別名)。
さらに、確認のための2回目のチェックも、既定では「再描画の対象になったビューだけ」を見ます。その結果、以前の版なら NG0100 が出たコードでも、v22 の既定ではエラーにならず、画面だけが更新されないままになりました。同じ部品を3つの設定で動かした結果です(TestBed で、部品を直接作成)。
| パターン | 既定(OnPush) | Eager | exhaustive: true |
|---|---|---|---|
① ngAfterViewInit で値を変更 |
エラーなし。表示は「読み込み中」のまま | NG0100 | NG0100 |
② 子の ngOnInit から親の値を変更 |
エラーなし。表示は「待機中」のまま | NG0100 | NG0100 |
③ getter が Math.random() を返す |
エラーなし | NG0100 | NG0100 |
| ④ 毎回新しい配列(中身は同じ)を返す | エラーなし | エラーなし | エラーなし |
⑤ afterNextRender で普通のフィールドを変更 |
エラーなし。表示は更新されないまま | NG0100 | NG0100 |
⑥ 値を Signal にして ngAfterViewInit で set |
「完了」と表示 | 「完了」と表示 | 「完了」と表示 |
また、実際の画面(ng new の既定構成)では、Eager の子コンポーネントでも、親の App が OnPush(既定)だと NG0100 は出ませんでした。 確認のチェックが、更新の対象にならなかった親の下までたどらないためです。
見逃さないための設定:provideCheckNoChangesConfig
provideCheckNoChangesConfig({ exhaustive: true }) を入れると、OnPush のビューも含めて全部を確認するようになり、上の表の右の列のとおり NG0100 が出ました。 angular.dev では v20.0 からの開発者プレビュー(developer preview)の API です。
// src/app/app.config.ts
import { ApplicationConfig, isDevMode, provideBrowserGlobalErrorListeners, provideCheckNoChangesConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
// 開発中だけ、OnPush のビューも含めて全部を確認する
...(isDevMode() ? [provideCheckNoChangesConfig({ exhaustive: true })] : []),
],
};
angular.dev の zoneless のガイドでは、interval(ミリ秒)を付けて定期的に確認させ、「変更を Angular に伝えずに値を変えた」バインディングを見つける使い方も紹介されています。
テストで使うときは、TestBed の providers に入れます。
beforeEach(() => TestBed.configureTestingModule({
providers: [provideCheckNoChangesConfig({ exhaustive: true })],
}));
再現パターンと、それぞれのエラー
ここからのコードは、エラーを出すために changeDetection: ChangeDetectionStrategy.Eager を付けています。v22 の既定(OnPush)では、同じコードでエラーが出ずに表示が前の値のままになります。
① ngAfterViewInit でテンプレートの値を変える
@Component({
selector: 'app-loading',
changeDetection: ChangeDetectionStrategy.Eager,
template: `<p>{{ message }}</p>`,
})
export class AfterViewInitBug implements AfterViewInit {
message = '読み込み中';
ngAfterViewInit() {
this.message = '完了'; // ← 描いたあとに変えている
}
}
NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '読み込み中'. Current value: '完了'. Expression location: _AfterViewInitBug component.
ngAfterViewInit は、そのコンポーネントのビューを描き終わったあとに呼ばれます。ここで変えた値は、その回の描画には間に合いません。ライフサイクルの順番は、関連記事「Angularライフサイクル一覧|ngOnInitなどの実行順をログで確認」で扱っています。
② 子コンポーネントが親の値を変える
@Component({ selector: 'app-child', template: `子` })
export class Child implements OnInit {
@Output() ready = new EventEmitter<string>();
ngOnInit() { this.ready.emit('子の準備OK'); } // ← 親の表示中の値を変える
}
@Component({
selector: 'app-parent',
changeDetection: ChangeDetectionStrategy.Eager,
imports: [Child],
template: `<h1>{{ status }}</h1><app-child (ready)="status = $event" />`,
})
export class ChildChangesParent { status = '待機中'; }
NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '待機中'. Current value: '子の準備OK'. Expression location: _ChildChangesParent component.
親の {{ status }} は、子より先に描かれます。そのあと子の ngOnInit が親の値を変えるので、親の表示が前の値のままになります。子が親を inject() して直接書き換える場合も、同じ形になります。
③ getter・メソッドが呼ぶたびに違う値を返す
@Component({
selector: 'app-random',
changeDetection: ChangeDetectionStrategy.Eager,
template: `<p>{{ now }}</p>`,
})
export class GetterBug {
get now() { return Math.random(); } // new Date() や Date.now() も同じ
}
NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '0.07320059597506878'. Current value: '0.2808546552137309'. Expression location: _GetterBug component.
④ 毎回新しい配列・オブジェクトを返す場合(エラーにならない)
get list() { return ['a', 'b']; } のように、呼ぶたびに新しい配列を返しても、中身が同じなら NG0100 にはなりませんでした。 v22.2.2 の確認のチェックは、配列なら中身を1つずつ比べ、配列以外のオブジェクトどうしは「同じ」とみなす作りでした(devModeEqual という関数)。
| getter が返すもの | NG0100 |
|---|---|
毎回違う数値・文字列(Math.random()・Date.now()) |
出る |
| 毎回新しい配列、中身は同じ | 出ない |
毎回新しいオブジェクト({ a: 1 }) |
出ない |
| 中身が変わる配列(並び替え結果がずれるなど) | 中身が違えば出る |
エラーにならなくても、子コンポーネントへの input は毎回「新しい値」として渡ります。値は computed() で持つほうが、無駄な更新がありません。
⑤ afterNextRender の中で普通のフィールドを変える
constructor() {
afterNextRender(() => { this.message = '完了'; }); // ← Eager なら NG0100
}
afterNextRender は「次の描画が終わったあと」に1回だけ動く関数です。angular.dev では、描画後の DOM を外部のグラフライブラリに渡す例が載っています。ここで普通のフィールドを変えると、①と同じ理由で NG0100 になりました。 Signal に set した場合は、エラーにならず「完了」と表示されました。
原因別の直し方(一覧)
| 原因 | 直し方 | 確かめた結果 |
|---|---|---|
ngAfterViewInit で値を変える |
値を Signal にして set |
「完了」と表示、エラーなし |
| 同上 | 最初から決まる値なら ngOnInit か初期値で決める |
「完了」と表示、エラーなし |
| 子から親の値を変える | 親の値を Signal にして、(ready)="status.set($event)" |
「子の準備OK」と表示、エラーなし |
| 同上 | 状態をサービスか親に置き、子は読むだけにする | 設計の見直し |
| getter が毎回違う値 | 値をフィールドに入れる/computed() にする |
呼ぶたびに値が変わらなくなる |
afterNextRender で値を変える |
変える値を Signal にする | 「完了」と表示、エラーなし |
| DOM の大きさで表示を変えたい | afterNextRender で読み、結果を Signal に set |
次の描画で反映 |
Signal にする(いちばん手を入れる場所が少ない)
@Component({
selector: 'app-loading',
template: `<p>{{ message() }}</p>`,
})
export class Loading implements AfterViewInit {
message = signal('読み込み中');
ngAfterViewInit() {
this.message.set('完了'); // Angular に「変わった」と伝わる
}
}
Signal の値を変えると、そのビューが「もう一度描く対象」になり、Angular が同じ流れの中で描き直します。 そのため、確認のチェックの前に表示が「完了」になり、3つの設定のどれでもエラーになりませんでした。Signal の使い方は関連記事「Angular Signals使い方|v22対応の全API早見表と実行結果つきコード」で扱っています。
子から親:親の値を Signal にする
@Component({
selector: 'app-parent',
imports: [Child],
template: `<h1>{{ status() }}</h1><app-child (ready)="status.set($event)" />`,
})
export class Parent { status = signal('待機中'); }
表示は「子の準備OK」になりました。ただ、子が初期化のたびに親の状態を書き換える形そのものが、読みにくさの元になりがちです。初期値を親が持ち、子には input で渡す形にできないかを先に考えます。親子の受け渡しは、関連記事「Angular input・output・modelの使い方|親子の受け渡し」で扱っています。
子の要素を見てから表示を変える:afterNextRender+Signal
@Component({
selector: 'app-panel',
template: `<div #box>…</div><p>高さ:{{ height() }}px</p>`,
})
export class Panel {
private box = viewChild.required<ElementRef<HTMLElement>>('box');
height = signal(0);
constructor() {
afterNextRender({
read: () => this.height.set(this.box().nativeElement.offsetHeight),
});
}
}
afterNextRender には earlyRead・write・mixedReadWrite・read の4つの段階があり、angular.dev では「DOM を読むだけなら read」のように分けて書く方法が推奨されています。viewChild は関連記事「Angular viewChildの使い方|DOM取得とcontentChild」で扱っています。
setTimeout・detectChanges でごまかす方法の位置づけ
検索するとよく出てくる、setTimeout と ChangeDetectorRef.detectChanges() も、①のコンポーネントで試しました(Eager)。
| 書き方 | v22(zoneless)での結果 |
|---|---|
ngAfterViewInit() { setTimeout(() => this.message = '完了'); } |
画面は「読み込み中」のまま。ブラウザではエラーなし。TestBed で fixture.detectChanges() をもう一度呼ぶと NG0100 |
ngAfterViewInit() { this.message = '完了'; this.cdr.detectChanges(); } |
「完了」と表示、エラーなし |
this.message.set('完了')(Signal) |
「完了」と表示、エラーなし |
setTimeout は、zoneless では効きませんでした。 Zone.js があった頃は setTimeout が終わると自動で変更検知が走りましたが、angular.dev の zoneless のガイドが挙げる「変更検知を予約する通知」は、markForCheck()・ComponentRef.setInput・テンプレートで読んでいる Signal の更新・テンプレートのイベントなどで、setTimeout は入っていません。zoneless への切り替えは、関連記事「Angular zoneless移行手順|v22実測と更新されない3例の直し方」で扱っています。
detectChanges() は、表示を直せますが、描画をもう1回余分に行う書き方です。 エラーの元(描いたあとに値を変える流れ)は残るので、ほかの部品から同じ値を読んでいると、別の場所で同じ問題が起きます。Signal か設計の見直しで直せない場合の最後の手段、という位置づけです。markForCheck() との違いは、関連記事「Angular変更検知とOnPush|markForCheckの使い方」で扱っています。
よくある質問
Q. テストでだけ NG0100 が出て、ブラウザでは出ません。
A. TestBed の fixture.detectChanges() は、作ったコンポーネントを直接チェックし、確認のチェックもそのコンポーネントから始まります。ブラウザでは親(App)が OnPush だと確認のチェックが下まで届かず、エラーになりませんでした。どちらも表示は更新されないままなので、テストのエラーは不具合の知らせです。
Q. 本番で出ないなら、放っておいてもよいですか。 A. 本番ビルドでは確認のチェックが動かないだけで、表示が前の値のまま残ることは今回の再現でも変わりませんでした。直す対象です。
Q. ChangeDetectionStrategy.OnPush を付けたらエラーが消えました。
A. v22 の既定は OnPush なので、エラーが「見えなくなった」可能性があります。OnPush を明示した部品でも、provideCheckNoChangesConfig({ exhaustive: true }) を入れると NG0100 が出て、表示は「読み込み中」のままでした。
Q. Expression location のクラス名に _ が付いています。
A. 開発ビルドでクラス名に付いたものでした。_AfterViewInitBug なら、ソースの AfterViewInitBug を探します。
まとめ
- NG0100 は、描いたあとにテンプレートの値が変わったことを、開発モードの確認チェックが見つけたエラー
- 本番ではエラーが出ないだけで、表示は更新されないままだった
- v22 は OnPush が既定。エラーが出ずに表示だけ更新されないことがあるので、開発中は
provideCheckNoChangesConfig({ exhaustive: true })で見つける - 直し方は Signal にするか、描く前に値を決める(
ngOnInit・初期値・computed()) setTimeoutは zoneless では画面が更新されなかった。detectChanges()は最後の手段
参考・出典
確認日はいずれも 2026年10月12日です。
- Angular「NG0100: Expression Changed After Checked」:https://angular.dev/errors/NG0100
- Angular API「ChangeDetectionStrategy」:https://angular.dev/api/core/ChangeDetectionStrategy
- Angular API「provideCheckNoChangesConfig」:https://angular.dev/api/core/provideCheckNoChangesConfig
- Angular API「afterNextRender」:https://angular.dev/api/core/afterNextRender
- Angular「Zoneless」:https://angular.dev/guide/zoneless
- Angular In Depth「Understanding the Expression Changed Error」:https://angularindepth.com/article/expression-changed-error-overview/
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「ExpressionChangedAfterItHasBeenCheckedの直し方」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。