ウェブサイト検索

javascript - もう一つのObservableに基づいてRXJSのscanオペレーターをリセットする方法


私は、仮想リストの最後のアイテムがレンダリングされた時に onScrollEnd イベントをトリガーするコンポーネントを持っています。このイベントは、新しい API リクエストを行って次のページを取得し、それらを scan 演算子を使って前の結果とマージします。

このコンポーネントには、onSearch イベントをトリガーする検索フィールドもあります。

検索イベントがトリガーされたときに、scan 演算子から以前に蓄積された結果をクリアするにはどうすればよいですか?それとも、ここでロジックをリファクタリングする必要がありますか?

const loading$ = new BehaviorSubject(false);
const offset$ = new BehaviorSubject(0);
const search$ = new BehaviorSubject(null);

const options$: Observable<any[]> = merge(offset$, search$).pipe(
  // 1. Start the loading indicator.
  tap(() => loading$.next(true)),
  // 2. Fetch new items based on the offset.
  switchMap(([offset, searchterm]) => userService.getUsers(offset, searchterm)),
  // 3. Stop the loading indicator.
  tap(() => loading$.next(false)),
  // 4. Complete the Observable when there is no 'next' link.
  takeWhile((response) => response.links.next),
  // 5. Map the response.
  map(({ data }) =>
    data.map((user) => ({
      label: user.name,
      value: user.id
    }))
  ),
  // 6. Accumulate the new options with the previous options.
  scan((acc, curr) => {
    // TODO: Dont merge on search$.next 
    return [...acc, ...curr]);
  }
);

// Fetch next page
onScrollEnd: (offset: number) => offset$.next(offset);
// Fetch search results
onSearch: (term) => {
  search$.next(term);
};

回答 #1

scanのstateを操作するには、古い状態と新しい更新を取得する高階関数を書くことができます。その後、マージ演算子と組み合わせます。こうすることで、副作用なしでクリーンなストリーム指向の解決策に従うことができます。

const { Subject, merge } = rxjs;
const { scan, map } = rxjs.operators;

add$ = new Subject();
clear$ = new Subject();

add = (value) => (state) => [...state, value];
clear = () => (state) => [];

const result$ = merge(
  add$.pipe(map(add)),
  clear$.pipe(map(clear))
).pipe(
  scan((state, innerFn) => innerFn(state), [])
)

result$.subscribe(result => console.log(...result))

add$.next(1)
add$.next(2)
clear$.next()
add$.next(3)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>

この方法は、rxjsの他のstateユースケースにも簡単に拡張および適応できます。

例(最後の項目を削除)

removeLast$ = new Subject()

removeLast = () => (state) => state.slice(0, -1);

merge(
  ..
  removeLast$.pipe(map(removeLast)),
  ..
)

回答 #2

チェーンを再構築するだけで、あなたが望むことを達成できると思います(簡単のために読み込みをトリガーする tap の呼び出しは省略しています):

search$.pipe(
  switchMap(searchterm =>
    concat(
      userService.getUsers(0, searchterm),
      offset$.pipe(concatMap(offset => userService.getUsers(offset, searchterm)))),
    ).pipe(
      map(({ data }) => data.map((user) => ({
        label: user.name,
        value: user.id
      }))),
      scan((acc, curr) => [...acc, ...curr], []),
    ),
  ),
);

search$ からのすべての放出は、それぞれ独自の scan を持つ新しい内部 Observable を作成し、空のアキュムレーターで開始します。

関連記事