У меня есть компонент, который запускает событие 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);
};
Это интересный поток. Если подумать, offset$ и search$ на самом деле являются двумя отдельными потоками, хотя и с разной логикой, поэтому их следует объединять в самом конце, а не в начале.
Кроме того, мне кажется, что поиск должен сбросить смещение до 0, и я не вижу этого в текущей логике.
Итак, вот моя идея:
const offsettedOptions$ = offset$.pipe(
tap(() => loading$.next(true)),
withLatestFrom(search$),
concatMap(([offset, searchterm]) => userService.getUsers(offset, searchterm)),
tap(() => loading$.next(false)),
map(({ data }) =>
data.map((user) => ({
label: user.name,
value: user.id
})),
scan((acc, curr) => [...acc, ...curr])
);
const searchedOptions$ = search$.pipe(
tap(() => loading$.next(true)),
concatMap(searchTerm => userService.getUsers(0, searchterm)),
tap(() => loading$.next(false)),
map(({ data }) =>
data.map((user) => ({
label: user.name,
value: user.id
})),
);
const options$ = merge(offsettedOptions, searchedOptions);
Посмотрите, работает ли это или имеет ли смысл. Я могу упустить какой-то контекст.
Я думаю, вы могли бы добиться того, чего хотите, просто реструктурировав свою цепочку (я опускаю вызовы 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$
создаст новый внутренний Observable с собственным scan
, который будет начинаться с пустого аккумулятора.
Спасибо, что нашли время ответить, Мартин. Я не заставил его работать полностью, как я хотел, используя ваше решение, но оно вдохновило меня, и я нашел рабочее решение. Вы можете проверить это здесь, дайте мне сейчас, если вы считаете, что это можно улучшить: stackblitz.com/edit/rxjs-поиск-смещение
TBH Я считаю, что это лучшее решение. Он держит все ориентированным на поток и не полагается ни на что, кроме самого потока, для сброса сканирования.
Это лучшее решение, но я не смог заставить его работать в своем приложении. Наблюдаемый поток никогда не подписывался, и звонки не запускались, я реализовал другой подход. Но это чище, мне, возможно, придется повторить это, когда крайний срок будет немного гибким.
Нашел рабочее решение: я проверяю текущее смещение, используя withLatestFrom
перед оператором scan
, и при необходимости сбрасываю аккумулятор на основе этого значения.
Это действительно очень лаконичное и чистое решение.
Чтобы управлять state
из scan
, вы можете написать функции высшего порядка, чтобы получить старое состояние и новое обновление. Затем объедините с оператором сливаться. Таким образом, вы придерживаетесь чистого решения, ориентированного на поток, без каких-либо побочных эффектов.
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.info(...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>
Этот метод можно легко расширить и/или адаптировать для других state
вариантов использования в rxjs.
Пример (удалить последний элемент)
removeLast$ = new Subject()
removeLast = () => (state) => state.slice(0, -1);
merge(
..
removeLast$.pipe(map(removeLast)),
..
)
Спасибо за ответ Джесси. Я не заставил его работать полностью, как я хотел, используя ваше решение, но оно вдохновило меня, и я нашел рабочее решение. Вы можете проверить это здесь, дайте мне сейчас, если вы считаете, что это можно улучшить: stackblitz.com/edit/rxjs-поиск-смещение