- Angular
Angularライフサイクル一覧|ngOnInitなどの実行順をログで確認
Angularのライフサイクルとは、コンポーネントが作られてから破棄されるまでの間に、Angularが決まった順番で呼び出すメソッド(フック)の流れのことです。 初回表示では constructor → ngOnChanges → ngOnInit → ngDoCheck → ngAfterContentInit → ngAfterContentChecked → ngAfterViewInit → ngAfterViewChecked → afterNextRender の順に進み、破棄のときに ngOnDestroy が呼ばれます。
この記事では、すべてのフックに console.log を入れた親子コンポーネントを実際に動かし、初回表示・入力変更・破棄の3場面のログをそのまま掲載します。親と子でどちらが先に呼ばれるか、afterNextRender・effect・DestroyRef・takeUntilDestroyed がどこに入るか、サーバーサイドレンダリング(SSR)で呼ばれるものはどれかを、ログから読み取って表にまとめました。
確認環境:Ubuntu 24.04 / Node.js 22.22.2 / Angular 21.2.25(@angular/cli 21.2.24、ng new の既定構成で zone.js なし)/ TypeScript 5.9.3 / Chromium 141(Playwright 1.56.0 で操作)。SSR は @angular/ssr 21.2.24 で確認しています。
Angularライフサイクルの早見表
| 順番 | フック | 呼ばれるタイミング | 回数 | 主な用途 | SSR |
|---|---|---|---|---|---|
| 1 | constructor | クラスが生成されたとき | 1回 | inject、effect や afterNextRender の登録 | 呼ばれる |
| 2 | ngOnChanges | 入力(input)の値が変わったとき。初回は ngOnInit の前 | 変更のたび | 入力の変化に応じた処理 | 呼ばれる |
| 3 | ngOnInit | 入力が最初にセットされた直後 | 1回 | 入力を使った初期化、データ取得の開始 | 呼ばれる |
| 4 | ngDoCheck | 変更検知のたび | 何度も | 独自の変更判定(軽い処理だけ) | 呼ばれる |
| 5 | ngAfterContentInit | 投影されたコンテンツの初期化後 | 1回 | contentChild の結果を使う初期化 | 呼ばれる |
| 6 | ngAfterContentChecked | 投影コンテンツの確認のたび | 何度も | 投影コンテンツの変化の確認 | 呼ばれる |
| 7 | ngAfterViewInit | 自分のテンプレート(子を含む)の初期化後 | 1回 | viewChild の結果を使う初期化 | 呼ばれる |
| 8 | ngAfterViewChecked | テンプレートの確認のたび | 何度も | 表示後の確認(軽い処理だけ) | 呼ばれる |
| 9 | afterNextRender | 次の描画が終わったあと | 1回 | DOM の計測、外部ライブラリの初期化 | 呼ばれない |
| 10 | afterEveryRender | 描画が終わるたび | 何度も | 描画のたびに必要な DOM 同期 | 呼ばれない |
| 最後 | ngOnDestroy | 破棄される直前 | 1回 | 購読解除、タイマー停止 | 呼ばれる |
「SSR」の列は、この記事の後半でサーバー側のログを出して確かめた結果です。まず覚えるのは ngOnInit(初期化)・ngOnChanges(入力の変化)・ngOnDestroy(後片付け)の3つで、DOM に触れる処理は afterNextRender に書く、と整理すると迷いにくくなります。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angularライフサイクル一覧|ngOnInitなどの実行順をログで確認」
実行順を確かめるコード(全フックにログを入れた親子)
親コンポーネントが子コンポーネントに入力 name を渡し、子の中にコンテンツを投影する構成です。フックのほかに、effect・afterNextRender・afterEveryRender・DestroyRef・takeUntilDestroyed もコンストラクターで登録して、ログの位置を比べます。
// src/app/lifecycle.ts
import {
Component, DestroyRef, SimpleChanges, afterEveryRender, afterNextRender,
effect, inject, input, signal,
OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { finalize, interval } from 'rxjs';
@Component({
selector: 'app-child',
template: `<p>子: {{ name() }}</p><ng-content />`,
})
export class Child implements OnChanges, OnInit, DoCheck, AfterContentInit,
AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
name = input('');
constructor() {
console.log('[子] constructor name=' + JSON.stringify(this.name()));
effect(() => console.log('[子] effect name=' + this.name()));
afterNextRender(() => console.log('[子] afterNextRender'));
afterEveryRender(() => console.log('[子] afterEveryRender'));
inject(DestroyRef).onDestroy(() => console.log('[子] DestroyRef.onDestroy'));
interval(200)
.pipe(takeUntilDestroyed(), finalize(() => console.log('[子] interval 停止(takeUntilDestroyed)')))
.subscribe();
}
ngOnChanges(changes: SimpleChanges) {
console.log('[子] ngOnChanges ' + JSON.stringify(changes));
}
ngOnInit() { console.log('[子] ngOnInit name=' + this.name()); }
ngDoCheck() { console.log('[子] ngDoCheck'); }
ngAfterContentInit() { console.log('[子] ngAfterContentInit'); }
ngAfterContentChecked() { console.log('[子] ngAfterContentChecked'); }
ngAfterViewInit() { console.log('[子] ngAfterViewInit'); }
ngAfterViewChecked() { console.log('[子] ngAfterViewChecked'); }
ngOnDestroy() { console.log('[子] ngOnDestroy'); }
}
@Component({
selector: 'app-parent',
imports: [Child],
template: `
<button id="change" (click)="name.set('B')">入力を変更</button>
<button id="toggle" (click)="show.set(false)">子を破棄</button>
@if (show()) {
<app-child [name]="name()"><span>投影された内容</span></app-child>
}
`,
})
export class Parent implements OnInit, DoCheck, AfterContentInit,
AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
name = signal('A');
show = signal(true);
constructor() {
console.log('[親] constructor');
effect(() => console.log('[親] effect name=' + this.name()));
afterNextRender(() => console.log('[親] afterNextRender'));
afterEveryRender(() => console.log('[親] afterEveryRender'));
inject(DestroyRef).onDestroy(() => console.log('[親] DestroyRef.onDestroy'));
}
ngOnInit() { console.log('[親] ngOnInit'); }
ngDoCheck() { console.log('[親] ngDoCheck'); }
ngAfterContentInit() { console.log('[親] ngAfterContentInit'); }
ngAfterContentChecked() { console.log('[親] ngAfterContentChecked'); }
ngAfterViewInit() { console.log('[親] ngAfterViewInit'); }
ngAfterViewChecked() { console.log('[親] ngAfterViewChecked'); }
ngOnDestroy() { console.log('[親] ngOnDestroy'); }
}
ルートコンポーネントのテンプレートに app-parent を1つ置き、ng build --configuration development でビルドしたものをブラウザーで開いて、コンソールの出力を記録しました。親には入力がないので、親の ngOnChanges は実装していません(入力のないコンポーネントでは呼ばれないためです)。
場面1:初回表示のログ
ページを開いた直後の出力です。
[親] constructor
[親] ngOnInit
[親] ngDoCheck
[親] effect name=A
[親] ngAfterContentInit
[親] ngAfterContentChecked
[子] constructor name=""
[子] ngOnChanges {"name":{"currentValue":"A","firstChange":true}}
[子] ngOnInit name=A
[子] ngDoCheck
[子] effect name=A
[子] ngAfterContentInit
[子] ngAfterContentChecked
[子] ngAfterViewInit
[子] ngAfterViewChecked
[親] ngAfterViewInit
[親] ngAfterViewChecked
[子] afterNextRender
[子] afterEveryRender
[親] afterNextRender
[親] afterEveryRender
このログから読み取れることを整理します。
| わかること | ログの該当箇所 |
|---|---|
| 子の constructor の時点では入力がまだ空 | [子] constructor name="" のあとに ngOnChanges で A が入る |
| 入力を使う初期化は ngOnInit に書く | [子] ngOnInit name=A で初めて値が読める |
| 初回の ngOnChanges は ngOnInit より前 | firstChange が true、previousValue は出力されない |
| effect の初回実行は ngDoCheck のあと、ngAfterContentInit の前 | 親・子ともに同じ位置 |
| 親の ngAfterViewInit は、子の全フックが終わったあと | 子の ngAfterViewChecked → 親の ngAfterViewInit |
| afterNextRender / afterEveryRender は全コンポーネントの ngAfterViewChecked のあと | 最後の4行 |
| 描画後コールバックは子が先、親があと | [子] afterNextRender → [親] afterNextRender |
親の ngOnInit から ngAfterContentChecked までが先に進み、そのあとで子が生成されて子のフックが一気に進み、最後に親の ngAfterViewInit に戻る、という「親で始まり、子をはさんで、親で終わる」形になっています。親の ngAfterViewInit が呼ばれた時点で、子の初期化はすべて完了していると考えてよい、というのがここでの要点です。
場面2:入力を変更したときのログ
「入力を変更」ボタンを押して、親の name を A から B に変えたときの出力です。
[親] ngDoCheck
[親] effect name=B
[親] ngAfterContentChecked
[子] ngOnChanges {"name":{"previousValue":"A","currentValue":"B","firstChange":false}}
[子] ngDoCheck
[子] effect name=B
[子] ngAfterContentChecked
[子] ngAfterViewChecked
[親] ngAfterViewChecked
[子] afterEveryRender
[親] afterEveryRender
- Init と付くフック(ngOnInit、ngAfterContentInit、ngAfterViewInit)と afterNextRender は呼ばれません。 1回だけのフックだからです。
- ngOnChanges には、previousValue(前の値)と currentValue(今の値)が入り、firstChange は false になります。
- effect は、読み取っているシグナル(name)が変わったので再実行されています。
- Checked と付くフックと afterEveryRender は、更新のたびに呼ばれます。
場面3:子を破棄したときのログ
「子を破棄」ボタンで @if の条件を false にしたときの出力です。
[親] ngDoCheck
[親] ngAfterContentChecked
[子] ngOnDestroy
[子] DestroyRef.onDestroy
[子] interval 停止(takeUntilDestroyed)
[親] ngAfterViewChecked
[親] afterEveryRender
- 子では ngOnDestroy → DestroyRef.onDestroy で登録したコールバック → takeUntilDestroyed による購読の終了の順に進みました。
- 子の afterEveryRender は、破棄後は呼ばれていません。登録したコンポーネントが破棄されると自動で解除されます。
- このあと「入力を変更」を押しても、出力は親の ngDoCheck・effect・Checked 系・afterEveryRender だけで、子の effect や ngOnChanges は出ませんでした。effect はコンポーネントと一緒に片付けられます。
- 親は破棄されていないので、親の Checked 系と afterEveryRender だけが続きます。
実行順の早見表(3場面のまとめ)
上の3つのログを1つの表にまとめます。数字はその場面の中での順番です。
| フック | 初回表示(親) | 初回表示(子) | 入力変更(親) | 入力変更(子) | 子の破棄(子) |
|---|---|---|---|---|---|
| constructor | 1 | 7 | ― | ― | ― |
| ngOnChanges | ―(入力なし) | 8 | ― | 4 | ― |
| ngOnInit | 2 | 9 | ― | ― | ― |
| ngDoCheck | 3 | 10 | 1 | 5 | ― |
| effect | 4 | 11 | 2 | 6 | ― |
| ngAfterContentInit | 5 | 12 | ― | ― | ― |
| ngAfterContentChecked | 6 | 13 | 3 | 7 | ― |
| ngAfterViewInit | 16 | 14 | ― | ― | ― |
| ngAfterViewChecked | 17 | 15 | 9 | 8 | ― |
| afterNextRender | 20 | 18 | ― | ― | ― |
| afterEveryRender | 21 | 19 | 11 | 10 | ― |
| ngOnDestroy | ― | ― | ― | ― | 1 |
| DestroyRef.onDestroy | ― | ― | ― | ― | 2 |
| takeUntilDestroyed の終了 | ― | ― | ― | ― | 3 |
流れを図にすると次のようになります。
flowchart TD
A[親 constructor] --> B[親 ngOnInit / ngDoCheck / effect]
B --> C[親 ngAfterContentInit / Checked]
C --> D[子 constructor]
D --> E[子 ngOnChanges / ngOnInit / ngDoCheck / effect]
E --> F[子 ngAfterContentInit / Checked]
F --> G[子 ngAfterViewInit / Checked]
G --> H[親 ngAfterViewInit / Checked]
H --> I[子 afterNextRender / afterEveryRender]
I --> J[親 afterNextRender / afterEveryRender]
各フックの役割と書き方
constructor と ngOnInit の使い分け
ログのとおり、constructor の時点では入力がまだセットされていません(name は初期値の空文字でした)。そのため、次のように分けると整理しやすくなります。
| 書く場所 | 向いている処理 |
|---|---|
| constructor(またはフィールド初期化) | inject による依存の取得、effect・afterNextRender・takeUntilDestroyed などの登録、入力に依存しない初期値 |
| ngOnInit | 入力の値を使った初期化、入力をもとにしたデータ取得の開始 |
inject・effect・afterNextRender・takeUntilDestroyed(引数なし)は「インジェクションコンテキスト」と呼ばれる範囲、つまりコンストラクターやフィールド初期化の中で呼ぶ必要があります。ngOnInit の中で使いたいときは、あらかじめ inject(DestroyRef) や inject(Injector) をフィールドに持っておき、それを引数で渡します。
ngOnChanges:入力の変化を受け取る
ngOnChanges は、入力が変わるたびに SimpleChanges を受け取ります。ログに出た中身は次のとおりです。
| 場面 | 出力された SimpleChanges |
|---|---|
| 初回 | {"name":{"currentValue":"A","firstChange":true}} |
| 変更時 | {"name":{"previousValue":"A","currentValue":"B","firstChange":false}} |
特定の入力だけに反応する書き方は次のとおりです。親で name を A から B に変えると「name が A から B に変わりました」と出力されました。
ngOnChanges(changes: SimpleChanges) {
const c = changes['name'];
if (c && !c.firstChange) {
console.log(`name が ${c.previousValue} から ${c.currentValue} に変わりました`);
}
}
今回の子コンポーネントは input()(シグナル入力)で入力を定義していますが、ngOnChanges はそのまま呼ばれました。シグナル入力の場合は、同じ目的を effect や computed でも書けるので、新しく書くコードでは「派生値は computed、副作用は effect、前の値との比較が要るときは ngOnChanges」と使い分けると短くまとまります。
ngDoCheck と Checked 系:呼ばれる回数が多いフック
ngDoCheck・ngAfterContentChecked・ngAfterViewChecked は、変更検知が走るたびに呼ばれます。上のログでも、ボタンを1回押すたびに親子それぞれで1回ずつ出力されました。ここに書く処理は軽い比較だけにとどめ、データ取得や重い計算は ngOnInit や effect に置くのが基本です。
ngAfterContentInit と ngAfterViewInit の違い
| フック | 対象 | 使う場面 |
|---|---|---|
| ngAfterContentInit | 親から投影されたコンテンツ(ng-content に入る部分) |
contentChild / contentChildren の結果を使う初期化 |
| ngAfterViewInit | 自分のテンプレートに書いた要素と子コンポーネント | viewChild / viewChildren の結果を使う初期化 |
ログでは、子の ngAfterContentInit(12番目)が ngAfterViewInit(14番目)より先でした。コンテンツが先、ビューがあとと覚えておくと順番を思い出しやすくなります。
afterNextRender と afterEveryRender の位置づけ
afterNextRender と afterEveryRender は、クラスのメソッドではなく、コンストラクターなどで登録する関数です。ログのとおり、アプリ全体のコンポーネントの ngAfterViewChecked が終わったあとに呼ばれます。Angular 日本語版ドキュメントのライフサイクルのページでも、これらは「レンダリング」の段階として、変更検知のフックとは別に整理されています。
| 関数 | 回数 | 向いている処理 |
|---|---|---|
| afterNextRender | 次の描画後に1回 | 要素の幅や位置の計測、グラフや地図など DOM を必要とする外部ライブラリの初期化、フォーカス移動 |
| afterEveryRender | 描画のたび | 描画のたびに DOM と状態を合わせる処理(呼ばれる回数が多いので軽く保つ) |
フェーズを指定して読み書きの順番をそろえる
afterNextRender にオブジェクトを渡すと、earlyRead・write・mixedReadWrite・read の4つのフェーズに処理を分けられます。わざと逆順に書いて、実行順を確かめました。
import { Component, afterNextRender } from '@angular/core';
@Component({
selector: 'app-phase',
template: `<div id="box" style="width:120px">箱</div>`,
})
export class Phase {
constructor() {
afterNextRender({
read: () => console.log('4. read'),
mixedReadWrite: () => console.log('3. mixedReadWrite'),
write: () => console.log('2. write'),
earlyRead: () => console.log('1. earlyRead'),
});
afterNextRender(() => {
const w = document.getElementById('box')!.getBoundingClientRect().width;
console.log('幅 = ' + w);
});
}
}
1. earlyRead
2. write
3. mixedReadWrite
幅 = 120
4. read
書いた順ではなく、earlyRead → write → mixedReadWrite → read の順に実行されました。フェーズを指定しない afterNextRender(幅を出力しているほう)は mixedReadWrite の位置で実行されています。ドキュメントでは、フェーズを使う目的を「DOM 操作の順序を制御し、レイアウトのスラッシング(読み書きの繰り返しによる再計算)を最小限に抑える」ことと説明しています。DOM に書き込む処理は write、書き込み後の大きさを読む処理は read に置くと、読み書きがまとまります。
effect の位置づけ
effect は、中で読み取ったシグナルが変わるたびに再実行される関数です。ライフサイクルフックではありませんが、ログでは次の位置に入りました。
- 初回:ngOnInit・ngDoCheck のあと、ngAfterContentInit の前(入力の値 A を読めている)
- 更新時:ngDoCheck のあと、ngAfterContentChecked の前
- 破棄後:実行されない
入力が確定してから動くので、「入力が変わったら何かをする」処理を ngOnChanges の代わりに書けます。一方、DOM に触れる処理は、テンプレートの描画が済んでから呼ばれる afterNextRender のほうが向いています。
DestroyRef と takeUntilDestroyed:後片付けの書き方
後片付けには3つの書き方があります。
| 書き方 | 登録する場所 | 向いている場面 |
|---|---|---|
| ngOnDestroy | クラスのメソッド | 片付ける対象をフィールドで持っているとき |
inject(DestroyRef).onDestroy(fn) |
コンストラクターなど | 登録と片付けを同じ場所に書きたいとき、共通関数に切り出すとき |
takeUntilDestroyed() |
RxJS の pipe の中 | Observable の購読をコンポーネントの破棄に合わせて終わらせるとき |
破棄のときの順番を、もう1つのコンポーネントで確かめました。
import { Component, DestroyRef, OnDestroy, effect, inject } from '@angular/core';
@Component({
selector: 'app-destroy-order',
template: `<p>破棄の順番</p>`,
})
export class DestroyOrder implements OnDestroy {
destroyRef = inject(DestroyRef);
constructor() {
this.destroyRef.onDestroy(() => console.log('onDestroy 1つ目'));
this.destroyRef.onDestroy(() => console.log('onDestroy 2つ目'));
effect((onCleanup) => {
console.log('effect 実行');
onCleanup(() => console.log('effect の onCleanup'));
});
}
ngOnDestroy() { console.log('ngOnDestroy / destroyed=' + this.destroyRef.destroyed); }
}
このコンポーネントを @if で表示し、条件を false にしたときの出力です(1行目は初回表示時のものです)。
effect 実行
ngOnDestroy / destroyed=true
onDestroy 1つ目
onDestroy 2つ目
effect の onCleanup
ngOnDestroy が最初、続いて DestroyRef.onDestroy のコールバックが登録順、最後に effect の onCleanup という順でした。ngOnDestroy の中で destroyRef.destroyed を読むと、すでに true になっています。非同期処理の完了後に「もう破棄されているなら何もしない」と判定したいときに、この destroyed プロパティが使えます。
takeUntilDestroyed のコピペ用コード
import { Component, DestroyRef, OnInit, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({
selector: 'app-clock',
template: `<p>{{ count() }} 秒経過</p>`,
})
export class Clock implements OnInit {
private destroyRef = inject(DestroyRef);
count = signal(0);
constructor() {
// コンストラクター内では引数なしで使える
interval(1000).pipe(takeUntilDestroyed()).subscribe(() => this.count.update(n => n + 1));
}
ngOnInit() {
// コンストラクターの外では DestroyRef を渡す
interval(5000).pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
}
}
表示して2秒あまり待つと、画面は「2 秒経過」になりました。ngOnInit の中で引数なしの takeUntilDestroyed() を呼んだ場合は、次のエラーになります。
NG0203: takeUntilDestroyed() can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`. Find more at https://v21.angular.dev/errors/NG0203
場面3のログで、子が破棄されたときに「interval 停止(takeUntilDestroyed)」が出力されたとおり、購読はコンポーネントの破棄と同時に終わります。unsubscribe を ngOnDestroy に並べる書き方から置き換えると、登録と片付けが1行にまとまります。
SSRで呼ばれるフック・呼ばれないフック
ng new --ssr で作ったプロジェクトに同じ親子コンポーネントを置き、サーバー側で描画するモード(RenderMode.Server)にして、node dist/プロジェクト名/server/server.mjs で起動したサーバーにアクセスしました。サーバーのターミナルに出たログは次のとおりです。
[親] constructor
[親] ngOnInit
[親] ngDoCheck
[親] effect name=A
[親] ngAfterContentInit
[親] ngAfterContentChecked
[子] constructor name=""
[子] ngOnChanges {"name":{"currentValue":"A","firstChange":true}}
[子] ngOnInit name=A
[子] ngDoCheck
[子] effect name=A
[子] ngAfterContentInit
[子] ngAfterContentChecked
[子] ngAfterViewInit
[子] ngAfterViewChecked
[親] ngAfterViewInit
[親] ngAfterViewChecked
[子] ngOnDestroy
[子] DestroyRef.onDestroy
[子] interval 停止(takeUntilDestroyed)
[親] ngOnDestroy
[親] DestroyRef.onDestroy
| 種類 | サーバー側 | ブラウザー側(ハイドレーション時) |
|---|---|---|
| constructor | 呼ばれる | 呼ばれる |
| ngOnChanges / ngOnInit / ngDoCheck | 呼ばれる | 呼ばれる |
| ngAfterContentInit / ngAfterContentChecked | 呼ばれる | 呼ばれる |
| ngAfterViewInit / ngAfterViewChecked | 呼ばれる | 呼ばれる |
| effect | 呼ばれる | 呼ばれる |
| afterNextRender / afterEveryRender | 呼ばれない | 呼ばれる |
| ngOnDestroy / DestroyRef.onDestroy | 呼ばれる(描画の最後に) | 破棄のときに呼ばれる |
サーバー側では、afterNextRender と afterEveryRender だけが呼ばれませんでした。ドキュメントにも「レンダリングコールバックは、サーバーサイドレンダリング中またはビルド時の事前レンダリング中は実行されない」と書かれています。実際に、ngOnInit で window.innerWidth を読むコードを SSR で動かすと、サーバー側では「ReferenceError: window is not defined」になり、同じ読み取りを afterNextRender に置くとサーバー側では実行されず、ブラウザー側でだけ値が取れました。window などブラウザーにしかないものに触れる処理は afterNextRender の中に書くのが安心です。
もう1つ、サーバー側でも ngOnDestroy が呼ばれる点もログからわかります。サーバーは1回のリクエストごとにアプリを作って HTML を返し、そのあと破棄するためです。ngOnInit で始めたタイマーや購読は、サーバー側でも後片付けが実行されます。
同じページをブラウザーで開くと、ブラウザー側でも constructor から順に同じログが出て、最後に afterNextRender と afterEveryRender が呼ばれました。ngOnInit はサーバーとブラウザーで合計2回動くので、ngOnInit にデータ取得を書くときはその前提で設計します。
なお、手元でこのサーバーに localhost でアクセスする際は、環境変数 NG_ALLOWED_HOSTS=localhost を付けて起動しました。付けない場合はサーバーのログに「Header "host" with value "localhost:8781" is not allowed.」と出て、サーバー側の描画を行わずにブラウザー側の描画に切り替わりました。
どの処理をどこに書くか(対応表)
| やりたいこと | 書く場所 | 理由 |
|---|---|---|
| サービスを受け取る | フィールド初期化の inject | 最も早く、型も付く |
| 入力に依存しない初期値を決める | フィールド初期化 / constructor | 入力を待つ必要がない |
| 入力の値を使って初期化する | ngOnInit | constructor では入力が空(ログで確認) |
| 入力をもとにデータ取得を始める | ngOnInit、または入力シグナルを読む effect | 入力が確定してから動く |
| 入力が変わるたびに処理する | effect / computed、前の値が要るなら ngOnChanges | SimpleChanges に previousValue が入る |
| 投影されたコンテンツを調べる | ngAfterContentInit | コンテンツの初期化後 |
| 子コンポーネントのメソッドを呼ぶ | ngAfterViewInit 以降 | 子の初期化がすべて終わっている |
| 要素の大きさを測る、フォーカスを当てる | afterNextRender | 描画後、ブラウザーでだけ動く |
| 外部ライブラリを DOM に取り付ける | afterNextRender | SSR ではスキップされる |
| window などブラウザー専用の API に触れる | afterNextRender | サーバー側で実行されない |
| 購読を終わらせる | takeUntilDestroyed | 登録と片付けが1か所 |
| タイマーやイベントリスナーを外す | DestroyRef.onDestroy / ngOnDestroy | 破棄の直前に呼ばれる |
| 描画のたびに DOM と状態を合わせる | afterEveryRender | 描画のたびに呼ばれる(軽い処理に限る) |
つまずきやすい点と直し方
| 起きること | 原因 | 直し方 |
|---|---|---|
| constructor で入力が初期値のまま | 入力は constructor のあとでセットされる | ngOnInit か effect で読む |
| 配列に push しても ngOnChanges が呼ばれない | 入力の参照が変わっていない(push では出力なし、新しい配列の代入で ngOnChanges: items と出力) |
[...items, 追加分] のように新しい配列を渡す |
| Checked 系に書いた処理で画面が重くなる | 変更検知のたびに実行される | ngOnInit・effect・computed に移す |
| ngOnInit の中で takeUntilDestroyed() を呼ぶと NG0203 になる | インジェクションコンテキストの外で呼んでいる | takeUntilDestroyed(this.destroyRef) と DestroyRef を渡す |
SSR で ReferenceError: window is not defined になる |
サーバー側でも ngOnInit は動く | ブラウザー専用の処理を afterNextRender に移す |
よくある質問
Q. ngOnInit と constructor はどちらに初期化処理を書けばよいですか? A. 入力(input)の値を使うなら ngOnInit、使わないなら constructor やフィールド初期化で十分です。今回のログでも、constructor の時点では入力が初期値のままで、ngOnInit で初めて親から渡された値が読めました。
Q. 親と子では、どちらの ngOnInit が先に呼ばれますか? A. 親が先です。ただし ngAfterViewInit は子が先で、親はそのあとに呼ばれます。親の ngAfterViewInit の時点で、子の ngOnInit から ngAfterViewChecked まではすべて終わっています。
Q. ngAfterViewInit と afterNextRender はどう使い分けますか? A. viewChild の結果を使うだけなら ngAfterViewInit、DOM の大きさを測る・フォーカスを当てる・ブラウザー専用の API に触れるなら afterNextRender が向いています。afterNextRender はサーバー側の描画では呼ばれないので、SSR を使うアプリでは特に区別が役立ちます。
Q. ngOnDestroy と DestroyRef はどちらを使えばよいですか? A. どちらでも後片付けができます。ログでは ngOnDestroy が先、DestroyRef.onDestroy のコールバックがそのあとでした。登録した場所のすぐ近くに片付けを書きたいときや、共通関数の中から登録したいときは DestroyRef が便利です。
まとめ
- 初回表示の順番は constructor → ngOnChanges → ngOnInit → ngDoCheck → ngAfterContentInit → ngAfterContentChecked → ngAfterViewInit → ngAfterViewChecked → afterNextRender / afterEveryRender。
- constructor の時点では入力は空。入力を使う初期化は ngOnInit に書く。
- 親の ngOnInit は子より先、親の ngAfterViewInit は子よりあと。
- 更新時は ngOnChanges・ngDoCheck・Checked 系・afterEveryRender だけが呼ばれ、Init 系と afterNextRender は呼ばれない。
- effect は ngDoCheck のあと、ngAfterContentInit の前に初回実行され、シグナルの変化で再実行される。
- 破棄時は ngOnDestroy → DestroyRef.onDestroy → effect の onCleanup の順。購読は takeUntilDestroyed で終わらせる。
- SSR では afterNextRender / afterEveryRender だけが呼ばれない。ブラウザー専用の処理はここに書く。
参考・出典
- Angular 日本語版ドキュメント「ライフサイクル」 https://angular.jp/guide/components/lifecycle
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angularライフサイクル一覧|ngOnInitなどの実行順をログで確認」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。