サンプルコードで身につける: ライフサイクルフック — ngOnInit と ngOnDestroy
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1ngOnInit で初期データを取得する
ngOnInit() は、コンポーネントの表示準備が整った直後に1回だけ呼ばれるメソッドです。実務では「画面が開いたらAPIからデータを取ってくる」という初期化処理をここに書くのが定番です。implements OnInit を付けておくと、メソッド名の書き間違いをコンパイラが検出してくれます。
import { Component, OnInit, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
type Article = { id: number; title: string };
@Component({
selector: 'app-articles',
template: '@for (a of articles(); track a.id) { <p>{{ a.title }}</p> }',
})
export class ArticlesComponent implements OnInit {
private http = inject(HttpClient);
articles = signal<Article[]>([]);
ngOnInit() {
// 表示準備が整ったタイミングで初期データを取得する
this.http.get<Article[]>('/api/articles').subscribe((list) => {
this.articles.set(list);
});
}
}
2ngOnDestroy で interval を止める
ngOnDestroy() はコンポーネントが画面から取り除かれる直前に呼ばれます。ngOnInit で始めた interval の購読をここで解除しないと、部品が消えたあともタイマーが動き続けてメモリリークになります。「始めたものは消えるときに止める」という後片付けのペアを示す代表例です。
import { Component, OnInit, OnDestroy } from '@angular/core';
import { interval, Subscription } from 'rxjs';
@Component({
selector: 'app-clock',
template: '<p>表示から {{ seconds }} 秒経過</p>',
})
export class ClockComponent implements OnInit, OnDestroy {
seconds = 0;
private sub?: Subscription;
ngOnInit() {
// 表示されたらタイマーを開始する
this.sub = interval(1000).subscribe(() => this.seconds++);
}
ngOnDestroy() {
// 画面から消える直前に必ず止める。忘れるとメモリリークに
this.sub?.unsubscribe();
}
}
3constructor と ngOnInit の役割分担
constructor はインスタンスが作られる瞬間に呼ばれ、依存性の注入を受け取るだけの場所にとどめます。表示にかかわる初期化処理は、その後に呼ばれる ngOnInit に書くのが役割分担の基本です。呼ばれる順番をログで確かめると、2つのタイミングの違いが体感できます。
import { Component, OnInit, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-order-page',
template: '<p>注文ページ</p>',
})
export class OrderPageComponent implements OnInit {
// constructor の役目は「依存を受け取る」ことだけにとどめる
private http = inject(HttpClient);
constructor() {
console.log('1. constructor: インスタンスが作られた');
// ここで重い処理や通信はしない
}
ngOnInit() {
console.log('2. ngOnInit: 表示準備が整った');
// データ取得などの初期化処理はこちらに書く
}
}
4input の値が揃うのは ngOnInit から
親から input() で渡される値は、constructor の時点ではまだ確定していません。入力値を使った初期化は、値が揃ったあとに呼ばれる ngOnInit に書くのが安全です。「入力値に基づいてデータを取得する詳細画面」のような実務の定番場面で効いてくる知識です。
import { Component, OnInit, input } from '@angular/core';
@Component({
selector: 'app-user-detail',
template: '<p>ユーザーID: {{ userId() }}</p>',
})
export class UserDetailComponent implements OnInit {
// 親から <app-user-detail [userId]="7" /> のように渡される
userId = input.required<number>();
constructor() {
// この時点で userId() を読むとエラーになる(まだ値が届いていない)
}
ngOnInit() {
// ngOnInit なら入力値が確定している。ここから初期化に使える
console.log('受け取ったID:', this.userId());
}
}
5DestroyRef.onDestroy — モダンな後片付けの書き方
新しい Angular では、inject(DestroyRef) で取得した DestroyRef の onDestroy() に後片付けの関数を登録できます。implements OnDestroy を書かずに済み、後片付けを「始めた処理のすぐ隣」に書けるのが利点です。ngOnDestroy と動きは同じなので、どちらを使うかはチームのスタイルに合わせます。
import { Component, DestroyRef, OnInit, inject } from '@angular/core';
import { interval, Subscription } from 'rxjs';
@Component({
selector: 'app-monitor',
template: '<p>更新回数: {{ count }}</p>',
})
export class MonitorComponent implements OnInit {
count = 0;
private destroyRef = inject(DestroyRef);
ngOnInit() {
const sub: Subscription = interval(3000).subscribe(() => this.count++);
// ngOnDestroy を書く代わりに、破棄時の処理をその場で登録する
this.destroyRef.onDestroy(() => sub.unsubscribe());
}
}