KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

RxJSと仲良くなる

こんにちは、株式会社カケハシでおくすり連絡帳 Pocket Musubiの開発を担当している渡辺です。

RxJS

RxJSはリアクティブプログラミングのライブラリです。 リアクティブプログラミングって何かと言いますと、 データを受け取るたびに反応(リアクション)をするプログラムです。 たとえば、

of(1, 2, 3).subscribe({
  next: (value) => console.log(value),
});

とすると、1, 2, 3の順番にデータを受け取り、受け取ったデータごとにnextが反応します。 このデータの流れをStreamと呼びます。 Streamを購読(subscribe)して、データやイベントが流れてくるたびに反応します。

そして、Angularでの基本技術になります。 そのため、RxJSを理解しないとAngularで良いコードを書くのは難しいのです。

私たちはフロントエンドのフレームワークをプロダクトごとに選定していて、 私はAngularを利用したプロダクトを担当することが多く、このRxJSにはお世話になっています。

あまりRxJSに慣れていないと、"subscribeに処理をなんでも書いてしまう病"になってしまいます。考え方が同期処理と同じになっていて、subscribeで値を受け取って何かすると考えてしまいます。 RxJSではStreamをうまく扱うことが大切です。Streamをきちんと意識してプログラミングしてみましょう。

オペレーターを使おう

filter

たとえばsubscribeの中でゴリゴリif文が入ってしまっている、そんな時はfilterを使うことを検討しましょう。

this.values$.subscribe((value) => {
  if (value.valid) {
    // 処理
  }
});

のように書くならば、

this.values$.pipe(
  fileter((value) => value.valid),
)
  .subscribe((value) => {
    // 処理
  });

と書けますよね。 subscribeのnextコールバックの中で条件分岐がたくさんあると、処理が煩雑になってしまいメンテナブルではないコードになってしまうので、オペレーターを活用してシンプルなコードにしていきましょう。

mapやfilterはRxJSの基本のオペレーターなので、ぜひ使いこなしてください。

mergeMap

mergeMapは新しい値にObservableなものを返すときに使います。 たとえば、ある処理でhttp処理を間に挟みたいといった場合に使います。

this.headers$.subscribe((header) => {
   apiGateway.get(header).subscribe(result) => {

   };
});

上記だと、subscribeの中でsubscribeしていますね。 これだと、nextコールバックが呼ばれるたびにsubscribeを呼んでしまっています。 httpのgetの場合は一度nextのコールバックが呼ばれたらcompleteしますが、そうではない継続的に処理されるStreamの場合は、ムダにsubscribeしてしまっています。

this.headers$.pipe(
  margeMap((header) => apiGateway.get(header)),
).subscribe((result) => {
  //  結果の処理
});

にしましょう!

finalize

next/errorにかかわらず、最後に共通な処理をしたい場合はこれを使います。 必ず最後に呼ばれて終了処理をします。API通信等が終わってloadingアイコンを非表示にしたいとか、そういうときに使いましょう。

this.deleteData$(id).pipe(
  finalize(() => this.loading$.next(LOADED)),
).subscribe(() => {
  //結果の処理
});

と書ければ便利ですよね。

pairwise

pairwiseは最新の値と、その1つ前の値を処理するときに使います。 どれくらいの情報を取得できたか、のようなときに使います。

this.downloads$().pipe(
    pairwise()
).subscribe([prev, current] => {
    //前後の比較等の処理を書く
});

 combineLatest

いくつかのStreamの値を受け取って利用したいことってありますよね。 たとえばAPIを2つ叩いて、その結果両方を利用したいときです。 そんな時はcombineLatestを使いましょう。

combineLatest([users$, contents$])
  .subscribe(
    ([user, content]) => {
      // userとcontentを利用した処理
    },
  );

distinctUntilChanged

状態とかが定期的に流れてくるStreamで、この状態が変わったときに処理を行いたい、なんてことありませんか? その時はdistinctUntilChangedを使ってみましょう。 Streamの最後の値と比較して、異なる場合のみ処理を行います。

this.state$.pipe(
  distinctUntilChanged(),
).subscribe((state) => {
  //stateに合わせた処理
});

tap に注意

tapは副作用を記述する処理であり、あくまでもStreamに何か影響を与えるわけではありません。新しい値も返しません。新しい値を返す場合は、mapやmergeMapを使います。私はここで勘違いしてしまいバグを作ってしまったので注意しましょう!

Promise と混ぜない

toPromise()といった便利なメソッドがあり、これによりStreamをPromiseに変換できます。 しかし、このメソッドの甘い誘惑に駆られてはいけません。こちらを使うと、RxJS前提のAngularの中にPromiseが混ざることになり、非常に混乱します。ソースコードの処理の一貫性を保つためにも、できる限りそのままStreamを使うようにしましょう。

オペレーターを眺める習慣をつけよう

公式ドキュメントのオペレーターを眺めて、どんなオペレーターかを把握しておくと良いです。 いざというときに、「もしかしたらこれが使えるかも?」となったら、ダイアグラム図を眺めながら使ってみましょう。