リソースの破棄にDestroyRefを使用しよう

DestroyRef は Angular 16 で追加された機能の1つで、 Component 等のスコープが破棄される際に呼び出されるコールバックを登録することができます。本記事では DestroyRef について簡単に紹介します。


はじめに

2023年5月に様々な新機能が実装された Angular 16 がリリースされました。 DestroyRef は Angular 16 で追加された新機能の1つで、冒頭にも記載した通り Component や Directive などのライフサイクルを持つスコープが破棄される際に呼び出されるコールバックを登録することができる機能です。

Angular 16 以前にもスコープの破棄のライフサイクルイベントとして OnDestroy が存在しており、 Observable の購読解除など様々なクリーンアップ処理の実装に利用されてきました。筆者が最も Angular で実装したコードと言っても過言ではないかもしれません。

DestroyRef を使用することで OnDestroy に関するボイラープレートコードがかなりスッキリします。本記事ではそんな DestroyRef について簡単に紹介します。

環境情報

本記事は以下の環境にて検証を実施しています。

Angular CLI: 16.2.7
Node: 18.18.2
Package Manager: npm 9.8.1
OS: win32 x64

Angular: 16.2.10
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1602.7
@angular-devkit/build-angular   16.2.7
@angular-devkit/core            16.2.7
@angular-devkit/schematics      16.2.7
@angular/cli                    16.2.7
@schematics/angular             16.2.7
rxjs                            7.8.1
typescript                      5.1.6
zone.js                         0.13.3

DestroyRef を活用しよう

Angular 15 までお世話になったリソース破棄コード

Angular はクラスベースのコンポーネントが採用されており、リソースのクリーンアップを実施するには OnDestroy インターフェースを実装して処理を記述します。

FormControl の変更検知など RxJS の購読処理を実装している場合は、次の様にスコープ破棄を伝播させるための Obseravable を作成して利用することが大変に多いです。 RxJS の購読破棄を怠ると不具合やメモリリークに繋がりますので、このような処理は必須です。

// RxJS以外の場合

@Component({ ... })
export class AppComponent implements OnInit, OnDestroy {

  // 破棄可能なサービス
  readonly #serviceInstance = inject(DisposableService).create();

  ngOnInit() {
  }

  ngOnDestroy() {
    // サービスインスタンスのクリーンアップ処理
    this.#serviceInstance.dispose();
  }
}
// RxJSの場合

@Component({ ... })
export class AppComponent implements OnInit, OnDestroy {

  // スコープ破棄を伝播させるためのObservable
  readonly #destroy$ = new Subject<void>();

  ngOnInit() {
    // FormControlなどのObservableの購読処理
    // takeUntilオペレーターを使用すると簡単に購読破棄を管理できる
    const ctrl = new FormControl('');
    ctrl.valueChanges
      .pipe(takeUntil(this.#destroy$))
      .subscribe(value => {});
  }

  ngOnDestroy() {
    // destroy$のnextを呼び出すとtakeUntilしている購読が全て破棄される
    this.#destroy$.next();
    this.#destroy$.complete();
  }
}

Angular 16 からのリソース破棄コード

Angular 16 からはリソース破棄のコールバックを登録することができる DestroyRef が利用できます。 DestroyRef の登場により、リソース破棄を通知する Observable を用意する必要が無くなり、その目的だけに OnDestroy を実装する必要は無くなりました。

DestroyRef にはコールバックを登録するための onDestroy メソッドが定義されており、以前 OnDestroy に実装していたコードはこのメソッドにコールバックを登録することで実現できます。

先のコードを DestroyRef で書き換えるとこのようになります。

// RxJS以外の場合

@Component({ ... })
export class AppComponent implements OnInit {

  readonly #destroyRef = inject(DestroyRef);
  // 破棄可能なサービス
  readonly #serviceInstance = inject(DisposableService).create();

  ngOnInit() {
    // onDestroyコールバックにクリーンアップ処理を登録
    this.#destroyRef.onDestroy(() => {
      this.#serviceInstance.dispose();
    });
  }
}

通常のクリーンアップ処理は実装する場所が変わっただけですね。これだけであれば利点は薄いのですが、各種コンポーネントの中で直接利用するよりも DestroyRef を他の処理に渡してライフサイクル管理に使用すると便利だと思います。

続いて RxJS の場合です。以前は takeUntil オペレーターを使用して購読管理を行っていました。 Angular 16 からは DestroyRef を引数に取る takeUntilDestroyed オペレーターを使用することでよりスッキリと実装できます。

// RxJSの場合

@Component({ ... })
export class AppComponent implements OnInit {

  readonly #destroyRef = inject(DestroyRef);

  ngOnInit() {
    // FormControlなどのObservableの購読処理
    // オペレーターをtakeUntilからtakeUntilDestroyedへ変更
    const ctrl = new FormControl('');
    ctrl.valueChanges
      .pipe(takeUntilDestroyed(this.#destroyRef))
      .subscribe(value => {});
  }
}

OnDestroy を実装する必要が無くなりとても簡潔になりました。 DestroyRef の登場によってかなりの数の OnDestroy 実装を無くせるのではないでしょうか。

なお takeUntilDestroyed はコンストラクター内などの InjectionContext 内では引数の DestroyRef は省略可能ですので、更にシンプルに記述することが可能です。

DestroyRef を受け取る処理をテストする

最後に DestroyRef の使い方では無く DestroyRef を使用する何かを実装した場合のテストコードを備忘録として記載します。

DestroyRef は Component や Directive などの要素だけでなく EnvironmentInjector に対しても使用可能です。そのため EnvironmentInjector を生成・破棄することで DestroyRef を利用した処理のテストが実装できます。

// DestroyRefによって破棄処理が呼び出されるオブジェクト
export class SampleObject {

  #disposed: boolean = false;

  get disposed(): boolean {
    return this.#disposed;
  }

  constructor(destroyRef: DestroyRef) {
    destroyRef.onDestroy(() => {
      this.#disposed = true;
    });
  }

}


// EnvironmentInjectorからDestroyRefを生成して利用するテスト
describe('SampleObject', () => {

  it('should be disposed when DestroyRef is destroyed', () => {
    // EnvironmentInjectorの生成
    const injector = Injector.create({ providers: [] }) as EnvironmentInjector;
    // EnvironmentInjectorのDestroyRefを取得
    const destroyRef = injector.get(DestroyRef);
    const obj = new SampleObject(destroyRef);

    expect(obj.disposed).toBeFalse();

    // EnvironmentInjectorを破棄してDestroyRefのコールバックを呼び出す
    injector.destroy();

    expect(obj.disposed).toBeTrue();
  });
});

おわりに

Angular 16 の新機能の1つ DestroyRef についての紹介でした。

最近の Angular は利便性やパフォーマンス向上のための新機能が次々と追加されています。フレームワークの更新で多数書いてきたボイラープレートコードが減るのはとても嬉しいです。

執筆担当者プロフィール
中谷 大造

中谷 大造(日本ビジネスシステムズ株式会社)

情報システム部の中谷です。社内用スクラッチアプリの開発をしています。

担当記事一覧