Marginalia

Angular: Input を Observable で扱えるようにする Inputs Stream パターン

Angular コンポーネントへのインプット @Input() に渡される値の変化を、 Observable で扱いたいことは少なくない。今回は最近試していて手触りがよい @rx-angular/state を使ったインプットの Observable化を紹介する。このパターンを “Inputs Stream” パターンと名付けたい。

基本方針

このパターンは次の基本方針から構成される。

  • コンポーネントのインプットをsetterメソッドで実装する
  • setterメソッドは渡された値をコンポーネント内部のインプットストアに格納する
  • コンポーネントのロジックやテンプレートに使う値は、インプットストアを購読することでリアクティブに取り出す

inputs: RxState<Inputs>

今回は例として @rx-angular/state を使ったインプットストアの実装を示している。単に new RxState() しているだけなので特筆することはない。

private readonly inputs = new RxState<{ name: string }>();

Input setter

こちらもインプットストアの値を更新しているだけで特別なことはしていない。

@Input()
set name(value: string) {
  this.inputs.set({ name: value });
}

Use inputs

このパターンの利点はインプットの変更を Observable で購読できることにあるから、そのように使わないともったいない。同期的に扱うならそもそもこのパターンが不要である。

今回の例はぶっちゃけ同期的でもいい例だが、たとえば message の構築に非同期APIの呼び出しが必要なケースなどをイメージするとよい。

ngOnInit() {
  // initial state
  this.state.set({ message: '' });
  // bind inputs stream to component state stream
  this.state.connect(
    'message',
    this.inputs.select('name').pipe(
      map((name) => `Hello ${name}!`),
    ),
  );
}

Pros / Cons

Inputs Streamパターンの利点はざっくり以下の点が思いつく。

  • コンポーネントが同期的に直接持つフィールドを減らせる
    • つまり、なんらかの入力を受けてリアクティブに連動しない状態値を減らせる
    • 結果、同じ情報のアプリケーション内での多重管理が起きにくくなる
  • ほとんどのコンポーネントが持つフィールドがパターン化される
    • コンポーネントごとのインプットの差はインプットストアの型の違いに落とし込まれる
    • どのコンポーネントにも同じ名前で同じ使われ方のインプットストアがあるという状態
  • 他の RxJS ベースのライブラリとのやりとりに変換作業が不要になる

一方で、欠点として以下の点も思いつく。

  • 当然だが、 Observable が苦手なら難しい
  • 自前で BehaviorSubject など使ってインプットストアを実装してもいいが、汎用性をもたせようとすると結構大変なので現実的には何らかのライブラリに頼ることになる
    • 今回は @rx-angular/state を使ったが、当然他のものでもなんでもよい

とはいえ慣れるとコンポーネントの this.xxx に直接保持する状態がなくなることで振る舞いの予測可能性があがり、テストもしやすいように感じているので、ぜひおすすめしたい。

今回のサンプルもそうだが、コンポーネントが状態値を単一のストリームでテンプレートに渡す Single State Stream パターンとの相性もよいので、こちらも改めて紹介しておきたい。