🤧

Angular(RxJS)でよく使っているオペレータたち

2024/09/15に公開

最近、業務で何回もdialogフォームを書いていたら、書き方がテンプレ化してきたので備忘録。

ついでに、記事っぽくするためにRxJSの基本的なところからまとめてみる。

subscribeのコールバックたち

of(1,2,3).subscribe({
  next: (res) => console.log(res),
  error: (e) => console.error(e),
  complete: (res) => console.log(res)
})
  • next
    Observableが新しい値を発行するたびに呼び出される
  • error
    Observableがエラーを発行した時に呼び出される。
  • complete
    Observableが完了した時に一度だけ呼び出される。ストリームが正常に終了したことを示す。

下記のように何も指定しないとnextが呼ばれる。

of(1,2,3).subscribe((res) => console.log(res));

よく使うオペレータ

個人的なRxJSオペレータのスタメンたち

take()

指定した回数だけ値を流す。take(1)で最初の1回だけを流したい時に使うことが多い。

of(1,2,3)
  .pipe(take(1))
  .subscribe((res) => console.log(res));

// 1

takeUntil()

takeUntilに渡したObservableが値を発行すると、元のObservableのストリームを完了させる。逆に言うと、takeUntilに渡されたObservableが発行されるまで、元のObservableのストリームを流す。
componentの破棄のタイミングでunsubscribeしたい時に使うこと多い。

first()

条件に一致した値を1回だけ流す。take(1) + filter() のイメージ。
第二引数に一致しなかった場合のデフォルト値を設定できるみたい。いま知った。

of(1,2,3)
  .pipe(first((res) => res > 1))
  .subscribe((res) => console.log(res));

// 2

map()

流れてきたObservableの値を書き換える。Array.map()と同じ要領で使える。

of(1,2,3)
  .pipe(map((n) => n === 3 ? 'hello': n))
  .subscribe((res) => console.log(res));

// 1
// 2
// hello

tap()

副作用を行うためのオペレータ。mapがデータを加工し終わってから次のオペレータに移るのに対し、tapは流れてきたデータはそのまま次に流しつつ副作用的に処理を行う。tap内で発生したerrorはストリームには伝搬しない。

of(1,2,3).pipe(
  tap(() => {
    console.log('tap: start');
    setTimeout(() => {
      console.log('tap: finish');
    }, 1000);
  }),
).subscribe((res) => console.log(res));

// tap: start
// 1
// 2
// 3
// tap: finish

filter()

条件に一致する値だけを流す。Array.filter()と同じ要領で使える。

of(1,2,3)
  .pipe(filter((n) => n > 2))
  .subscribe((res) => console.log(res));

// 3

throwError(), catchError(), EMPTY

  • throwError
    Observable内でエラーを発行し、エラー終了させる。
  • catchError
    ストリームを流れてきたエラーをキャッチする。
  • EMPTY
    完全に空のObervableを生成し、すぐにcompleteされる。

catchエラー内でthrowErrorすると再度errorオブジェクトがストリームを流れ、次のcatchErrorまたはsubscribeのerrorコールバックまで流れる。

of(1,2,3)
  .pipe(
    map((n) => {
      if (n === 3) throw 'three!';
      return n;
    }),
    catchError((error) => {
      console.log('catchError');
      // errorをerrorのまま返す
      return throwError(() => e);
    })
  )
  .subscribe({
    next: (res) => console.log(res),
    error: (e) => console.error(e),
    complete: () => console.log('complete')
  });

// 1
// 2
// catchError
// three!

catchError内でofメソッドでObservableを返すと、通常通りsubscribeでnextとcompleteが呼ばれる。

of(1,2,3)
  .pipe(
    map((n) => {
      if (n === 3) throw 'three!';
      return n;
    }),
    catchError((error) => {
      console.log('catchError');
      // errorをただのobservableに変更
      return of(error);
    })
  )
  .subscribe({
    next: (res) => console.log(res),
    error: (e) => console.error(e),
    complete: () => console.log('complete')
  });

// 1
// 2
// catchError
// next three!
// complete

EMPTYを返すと即時にsubscribeのcompleteが呼ばれる。

of(1,2,3)
  .pipe(
    map((n) => {
      if (n === 3) throw 'three!';
      return n;
    }),
    catchError((error) => {
      console.log('catchError');
      // errorをEMPTYにして返す
      return EMPTY;
    })
  )
  .subscribe({
    next: (res) => console.log(res),
    error: (e) => console.error(e),
    complete: () => console.log('complete')
  });

// 1
// 2
// catchError
// complete

switchMap()

元のObservableを新しいObesrvableにすり替える。
HttpClientなど元々Observableを返すような処理と繋げやすくて重宝する。

of(1, 2, 3)
  .pipe(
    first((n) => n === 1),
    // 流れてきた値をパラメータとしてAPIを叩く
    switchMap((n) => this.http.get('/api/dummy/${n}'))
  )
  .subscribe((res) => console.log(res));

finalize()

通常終了でもエラー終了でも必ず最後に呼ばれるオペレータ。
ローディングを終了させるのに便利。

of(1,2,3)
  .pipe(
    map((n) => {
      if (n === 3) throw 'error!';
      return n;
    }),
    finalize(() => console.log('finalize'))
  )
  .subscribe({
    next: (res) => console.log(res),
    error: (e) => console.error(e),
    complete: () => console.log('complete')
  });

// 1
// 2
// error!
// finalize

dialogを閉じてapiを叩いてみる。

AngularMaterialのdialogでformを実装したとして、submitしてdialogが閉じた後の処理を書いてみる。

  1. そもそもdialog内のformがsubmitされていなければundefinedが返ってくるので、その時は即時にcompleteさせる。
  2. apiを叩く前にローディングUIに切り替える
  3. formのデータをリクエストボディに渡す
  4. エラーステータスが401の時だけalertを出してみる
  5. 正常にapiが返ってきたらsnackBarで通知してみる
  6. api(またはそれ以前の処理)がerrorであればconsoleに出力する
  7. ローディングUIを終了する
class DummyComponent {
  private http = inject(HttpClient);
  private dialog = inject(MatDialog);
  private snackBar = inject(MatSnackBar);

  // スピナー等のローディングUIを制御する用
  protected isProcessing$ = new BehavorSubject<boolean>(false);

  protected addData() {
    this.openDialog()
      .pipe(
        swtichMap(res) => {
          this.isProcessing$.next(true);
          this.http.post('/api/dummy', res);
        }),
        catchError((e: HttpErrorResponse) => {
          if (e.status === 401) {
            alert('認証エラーです');
          }
          return throwError(() => e);
        }),
        finalize(() => this.isProcessing$.next(false))
      )
      .subscribe({
        complete: () => this.snackBar.open('データを追加しました'),
        error: (e) => console.error(e),
      })
  }

  // dialogを開いた後、結果があれば返すメソッド。
  private openDialog() {
    return this.dialog.open(DummyDialogComponent)
      .afterClosed()
      .pipe(
        swtichMap((res) => {
           if (!res) return EMPTY;
           return of(res);
        })
      );
  }
}

Discussion