Dev Study
← 解説「ライフサイクルフック — ngOnInit と ngOnDestroy」に戻る

サンプルコードで身につける: ライフサイクルフック — 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());
  }
}