Angular Signalsの状態管理!実装パターンとRxJSの相互運用。

Angular Signalsの状態管理!実装パターンとRxJSの相互運用。

この記事では、Angularの新しい状態管理システムである「Signals」について分かりやすく説明したいと思う。

Angular 16にてプレビュー版としてリリースされたSignalsはシンプルながらエレガントな状態管理を可能にするシステムであり、新生Angularの数ある新機能の中でももっとも注目されている。

そんなAngularの行く末を決める重要な機能であるSignalsだが、その役割はいまいち理解されていない。また、既存のプロジェクトにてどのようにRxJSと併用していくのかイメージ出来ていない開発者も少なく無いだろう。

今回はそんなSignalsの基礎から実装方法、RxJSとの併用運用の方法について解説したいと思う。

Angular Signalsの基礎

まずはSignalsの基礎的な部分を確認しよう。

Angular Signalsとは?

Signalsはアプリケーション内のデータの状態を追跡するためのシステムであり、Signalsを使うことによってアプリの画面更新を最適化することができる

Signalsを使用することによりコードの記述をシンプルにすることができるうえに、画面レンダリングにおけるパフォーマンスの向上が期待できる。

また、Signalsはその仕組みがReactの状態管理システムであるReduxに似ているのでReactの経験者にとってはAngularの学習コストを下げることにも繋がるだろう。

SignalsはAngularの公式リアクティブフレームワークであるRxJsと役割が被る部分があるが、SignalsはRxJSを完全に置き換えるものでは無い。つまり、今後はRxJSとSignalsを併用して運用していく事となる。

Signalによる変更検知の仕組み

次にSignalの変更検知の仕組みを従来のzone.jsを使用した変更検知と比較しながら説明しよう。

以下の図はSignalsを使った変更検知の流れを表している。

Signalsを使った変更検知では変更があったデータから直接Angularに変更を通知するので、コンポーネントツリーの全体をチェックする必要が無い。

Signalsが備えるパブリッシュ・サブスクリプションの仕組みがこの効率化された情報伝達を可能としている。

Signalsのメリット

次にSignalsのメリットの部分を確認したい。

  • Signalsを使った変更検知ではコンポーネントツリー全体を確認する必要が無いので、処理に掛かる負担が軽減されパフォーマンスの向上が見込める。
  • Signalsのコード記述は非常にシンプルなため、コードベースをすっきりとさせる事ができる。
  • Signalsはコンポーネントの使用が終わった際に自動で廃棄させるのでメモリーリークを心配する必要が無い。

Angular Signalsの実装方法

それではコード例を使ってSignalの具体的な実装方法を確認しよう。

Signalの定義と初期化

シグナルを定義する場合はsignal関数を初期値と共に呼び出す。

message = signal("はじめまして。");

これが主なシグナルの定義の方法であり、こうして宣言されたシグナルは書き込み可能シグナル(Writable signal)と呼ばれる

シグナルの値を出力する際はシグナルの名前の後に丸カッコを付ける。

console.log("メッセージ: " + message());

また、テンプレートでシグナルの値を出力する際も丸カッコを付ける。

<div>{{ message() }}</div>

Signalの更新と変更

シグナルの値を更新する際はset関数かupdate関数を使用する。

単純に新しい値をセットする際はset関数を使う。

message.set("こんにちわ。");

現在の値を参照しながら新しい値をセットしたい場合はupdate関数を使用する。

message.update(value => value + "こんにちわ。");

オブジェクトを使用したSignalの例

Signalのデータ型としてオブジェクトを使用するパターンも確認しておこう。

 selectedUser = signal<User>(currentUser);   

新しいオブジェクトをセットする場合はset関数を使用する。

 selectedUser.set(newUser);   

オブジェクトのプロパティを更新する場合は以下のようにupdate関数とスプレッドオペレータを使用する。

selectedUser.update(user => ({ ...user, password: newPassword })); 

Signalの種類と使用方法

Signalにはいくつか種類がある。ここではComputed Signalとeffectを紹介したい。

Computed Signal

計算型シグナル(Computed Signal)は値を他のシグナルの値から計算するタイプの読み取り専用のシグナルだ。

Reduxの使用経験のある人は”Selector”と同じような存在だと考えると分かりやすいだろう。

計算型シグナルを定義する際はcomputed関数を使用する。以下の例は文字列から総文字数を計算するシグナルの例だ。

text = signal("ここに文字を入力する。");
totalCharacterNumber: Signal<number> = computed(() => this.text().length);

計算型シグナルは呼び出されるまで計算を実行しない。計算された値は基となるシグナルが変更されるまでの間キャッシュされるのでマシンに無駄な負荷が掛かることは無い

なお、計算型シグナルを複数使用する場合はシグナル同士が作用しあってループが発生しないように気を付ける必要がある。

Effect

エフェクト(Effect)はsignalに変更がおこった際に呼び出される関数(サイドエフェクト)を指定する。

エフェクトはコンポーネント内のいずれかのSignalが変更された際に、変更検知のプロセスの間に非同期で毎回呼び出される。

エフェクトを定義する際はeffect関数を使用する。

constructor() {
  effect(() => {
    console.log(`現在の値: ${ text() }`);
  });
}

エフェクトは上記のコードのようにコンストラクタで定義するか、もしくは下記のコードのようにフィールドメンバとして定義するのがいいだろう。

export class TextInputComponent {
  private logEffect = effect(() => {
    console.log(`現在の値: ${ text() }`);
  });
}

Injectorを使用すればコンストラクタ外の関数にも使用することができる。

constructor(private injector: Injector) {}

initLogger(): void {
  effect(() => {
    console.log(`現在の値: ${ text() }`);
  }, {injector: this.injector});
}

エフェクトはメモリーリークを引き起こす危険があるため、本当に必要な場面でのみ使用するようにする。

SignalsとRxJsの併用

最後にSignalsとRxJSをいかにプロジェクト内で併用していくかについて考えてみよう。

SignalsとRxJSの比較

まずはRxJSがどのようなフレームワークか再確認したうえで両者を比較してみよう。

RxJSSignals
メインのタイプObservablesignal
プロセス処理非同期型同期型
コンセプト多くのオペレーターが用意されており複雑な処理を扱える。シンプルな記述でデータの変更検知に重きを置く。

RxJSがAPIリクエストから画面の更新まで幅広い範囲のオペレーションをカバーするものであったのに対し、Signalsはビューに関わる部分に特化して開発されている。

SignalsとRxJSの相互運用

次にもう少し踏み込んでSignalsとRxJSが共存するプロジェクトのあり方について考えてみたい。

まず、理解しなければいけないのはSignalsはRxJSを完全に置き換えるために開発された訳ではない事だ。SignalsとRxJSは相互運用するのが前提でありそのためのパッケージも用意されている。

また、Angularは下位互換性を保証しているので、ひとつのコンポーネントにRxJSとSignalsが共存していてもまったく問題は無い。

APIリクエストにまつわる処理の多くはSignalsでは扱うのが難しい。 SignalsにはcombineLatestやswitchMapといった複数のストリームを扱うようなオペレーターは存在しない。

RxJsでは可能だがSignalsでは扱えない処理

  • 複数のストリームを融合する処理。
  • 非同期によるシークエンス処理。
  • エラー処理。
  • 遅延処理やイベントの抑制。

既存のAngularプロジェクトにおいては、画面に表示される部分から徐々にObservableをSignalsに置き換えていくのが現実的なシナリオだろう。

ObservableとSignalの変換

最後にObservableとSignalの相互変換についてコードの実例を交えながら解説したい。

SignalsにはRxJSとの相互運用のためのパッケージが用意されている。
RxJS Interop:: https://angular.dev/guide/signals/rxjs-interop

このパッケージにあるtoSignal関数とtoObservable関数を使用して、observableとsignalsを相互変換することができる。

toObservableの例

以下のコードはtoObservable関数の使用例だ。

customers = signal<Customer[]>([]);
customers$ = new Observable<Customer[]>();

constructor(private injector: Injector) {}

toObservable() {
  this.customers$ = toObservable(this.customers, {
    injector: this.injector
  });
}

toObservableとtoSignalはEffectsと同様にInjectionコンテクストでのみ使用が可能。この例ではInjectorをオプションとして渡すことで使用している。

toSignalの例

以下はtoSignal関数の使用例だ。この例ではフィールドを宣言する際にobservableをsignalに変換している。

  customers$ = new Observable<Customer[]>();
  customers = toSignal(this.customers$, { initialValue: [] as Customer[] });

signalでは初期値を必ず設定しなければいけないので、nullになる可能性もあるobservableではオプションとして初期値を設定している。