はじめに
RxJSのコードを読んでいたら、こんな書き方が出てきました。
const value = await firstValueFrom(source$);
Observableをawaitしているように見えて、最初は戸惑いました。Observableはsubscribeして使うものだと思っていたので、firstValueFromが何をしているのか気になって調べてみました。
firstValueFromは何をしているのか
firstValueFromは、Observableから最初に流れてきた値を1つだけ取り出して、Promiseとして返す関数です。
import { firstValueFrom } from 'rxjs';
const value = await firstValueFrom(source$);
RxJSのObservableは本来subscribeして使うものですが、値を1回だけ取りたいだけの場面でsubscribe/unsubscribeを手で書くのは面倒です。firstValueFromを使うと、その面倒な部分を関数に任せて、ObservableをawaitできるPromiseとして扱えます。
やっていることをイメージで表すと、次のコードに近いです。
import { first } from 'rxjs';
const value = await new Promise((resolve, reject) => {
source$.pipe(first()).subscribe({
next: resolve,
error: reject,
});
});
first()で最初の値だけに絞り込み、値が流れてきた時点でPromiseをresolveしています。これを毎回手書きするのは大変なので、firstValueFromはありがたい存在だと感じました。
値を出さずに完了した場合はエラーになる
Observableが何も値を発行せずに完了(complete)した場合、firstValueFromが返すPromiseはEmptyErrorでrejectされます(awaitすると例外として投げられます)。
値を出さない可能性があるObservableに使う場合は、defaultValueオプションでデフォルト値を指定できます。
const value = await firstValueFrom(source$, { defaultValue: null });
これを指定しておけば、Observableが空のまま完了してもエラーにならず、代わりにnull(指定した値)が返ってきます。
lastValueFromとの使い分け
似た関数にlastValueFromがあります。両者の違いは、どのタイミングで、どの値をPromiseの結果にするかです。
const source$ = of(1, 2, 3); // 同期的に1, 2, 3を流してcomplete
const first = await firstValueFrom(source$); // → 1
const last = await lastValueFrom(source$); // → 3
-
firstValueFrom… 最初に流れてきた値を受け取ったら、そこでunsubscribeする -
lastValueFrom… Observableが完了するまで待ち、最後に流れてきた値を受け取る
この違いから、使いどころも変わってきます。
- HTTPリクエストのように、1回だけ値が流れてすぐcompleteするものには
firstValueFrom - 複数回値が流れるObservableから、途中経過は不要で最終結果だけ欲しいときは
lastValueFrom
注意が必要なのは、lastValueFromは「最後の値」を知るためにObservableが完了するまで待つという点です。interval()のように値を流し続けて完了しないObservableにlastValueFromを使うと、いつまでもPromiseがresolveされません。
一方firstValueFromは最初の値が来た時点でunsubscribeするので、interval()のように最初の値が必ず発行されるObservableであれば、完了しないストリームにも使えます。ただし、値を一度も発行せず完了もしないObservable(NEVERや、値を流さないSubjectなど)に使った場合は、こちらもPromiseが未解決のまま残ります。
toPromise()との違い
以前はObservableが持つtoPromise()というメソッドがよく使われていましたが、現在は非推奨とされていて、firstValueFrom・lastValueFromへの置き換えが案内されています。
toPromise()には「Observableが何も値を流さずに完了した場合、undefinedを返す」という挙動があり、これが紛らわしいとされていました。値を取得できなかったのか、Observableがundefinedという値を流したのか、戻り値だけでは区別がつきません。
firstValueFrom・lastValueFromはこの点が改善されていて、defaultValueを指定していない場合、値を出さずに完了するとPromiseがEmptyErrorでrejectされる(awaitすると例外として投げられる)ので、値が取れなかったことにその場で気づけます。defaultValueを指定した場合は、代わりにその値でresolveされます。
もう一点、toPromise()は完了時点の最後の値でresolveする関数だったので、従来と同じ挙動を保ちたい場合はfirstValueFromではなくlastValueFromに置き換えるのが対応関係としては近くなります。最初の値だけが必要な場合にfirstValueFromを選ぶ、という使い分けです。
まとめ
-
firstValueFromはObservableから最初の値を1つだけ取り出し、Promiseとして返す関数 - 値を出さずに完了するとPromiseが
EmptyErrorでrejectされる。可能性がある場合はdefaultValueオプションで対処する -
lastValueFromは最後の値を返すために完了を待つ。完了もエラーもしないObservableではPromiseが未解決のまま残るので、firstValueFromとは使い分けが必要 - 非推奨になった
toPromise()の代わりとして案内されている
subscribeしか知らなかったので、Observableを1回だけの値としてPromise化できる関数があるのは便利だと感じました。lastValueFromとの違いは「完了を待つかどうか」で整理すると迷わずに済みそうです。
参考になったら いいね や ストック をお願いします!
同じような経験をされた方のコメントもお待ちしています。
参考
関連リンク
技術ブログでも学びや検証内容をまとめています。