📝

フロントからの検索リクエストをRxJSでいい感じに制御したい

2023/06/19に公開

バージョン

RxJS: 7.8.1

仕様

  1. 500ms待ってからリクエストを送る
  2. リクエスト中に後続リクエストが来た場合は後勝ち(前のリクエストをキャンセルする)
  3. 検索ワードが2文字未満の場合はリクエストを送らずに結果を空にする
  4. サーバーがエラーからエラーが返ってきた場合は結果を空にする

結論

こんな感じが良さそうです。

this.fetchSubject$.pipe(
  distinctUntilChanged(),
  tap(() => {
    this.items = []
    this.isLoading = true;
  }),
  switchMap(keyword => iif(
    () => keyword.length >= 2,
    from([keyword]).pipe(
      delay(500),
      mergeMap(keyword => this.fetch(keyword)),
    ),
    of([]),
  )),
).subscribe(items => {
  this.items = items
  this.isLoading = false;
})

ざっくり説明

  1. キーワードが変わるまでは処理しない
  2. 最初に前回の結果を空にして、ローディング状態にする
  3. 先行リクエストがある場合はキャンセルする(switchMap便利)
  4. キーワードが2文字未満の場合はリクエストを送らない
  5. 500ms待ってからリクエストを送る

ポイント

5.の処理について、debounceTimedebounceでは期待していた動きにはなりませんでした。

愛すべきプロトタイプたち

1. setTimeoutを使う

clearTimeoutunsubscribe を両方考えないといけないのが面倒なので🙅。
そもそもRxJSを全然活用できていない。

private search(keyword: string): void {
  if (this.fetchTimeoutId) {
    clearTimeout(this.fetchTimeoutId)
  }
  this.fetchSubscription?.unsubscribe();
  this.issues = [];
  this.isLoading = true;
  this.fetchTimeoutId = setTimeout(() => {
    this.fetchSubscription = this.fetch(keyword)
      .subscribe(issues => {
        this.issues = issues
        this.isLoading = false;
      })
  }, 500)
}

2. debounceTimeを使う

必要の無い時でも500msの待ち時間が発生してしまうので🙅。
検索リクエストを送らない場合は遅延させないようにしたい。

this.fetchSubject$.pipe(
  distinctUntilChanged(),
  tap(() => {
    this.items = []
    this.isLoading = true;
  }),
  debounceTime(500),
  switchMap(keyword => iif(
    () => keyword.length >= 2,
    this.fetch(keyword),
    of([]),
  )),
).subscribe(items => {
  this.items = items
  this.isLoading = false;
})

3. debounceを使う

だいぶ良い感じではあるけど、先行リクエストをキャンセルする前に500msの待ち時間が発生してしまうので🙅。
リクエストをキャンセルしてから500ms待つようにしたい。
keyword の長さチェックが2回入っているのもできればやめたい。

this.fetchSubject$.pipe(
  distinctUntilChanged(),
  tap(() => {
    this.items = []
    this.isLoading = true;
  }),
  debounce(keyword => timer(keyword.length >= 2 ? 500 : 0)),
  switchMap(keyword => iif(
    () => keyword.length >= 2,
    this.fetch(keyword),
    of([]),
  )),
).subscribe(items => {
  this.items = items
  this.isLoading = false;
})

終わりに

ここが違うよ、こうすればもっと簡単にできるよ、などありましたら、コメントをいただけるととてもありがたいです。

Discussion