Как сбросить оператор сканирования RXJS на основе другого Observable

У меня есть компонент, который запускает событие 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);
};
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
15
0
3 281
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Это интересный поток. Если подумать, 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);

Посмотрите, работает ли это или имеет ли смысл. Я могу упустить какой-то контекст.

Спасибо за ответ Джесси. Я не заставил его работать полностью, как я хотел, используя ваше решение, но оно вдохновило меня, и я нашел рабочее решение. Вы можете проверить это здесь, дайте мне сейчас, если вы считаете, что это можно улучшить: stackblitz.com/edit/rxjs-поиск-смещение

Ritchie 28.05.2019 19:54

Я думаю, вы могли бы добиться того, чего хотите, просто реструктурировав свою цепочку (я опускаю вызовы 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-поиск-смещение

Ritchie 28.05.2019 19:54

TBH Я считаю, что это лучшее решение. Он держит все ориентированным на поток и не полагается ни на что, кроме самого потока, для сброса сканирования.

dudewad 14.12.2020 19:34

Это лучшее решение, но я не смог заставить его работать в своем приложении. Наблюдаемый поток никогда не подписывался, и звонки не запускались, я реализовал другой подход. Но это чище, мне, возможно, придется повторить это, когда крайний срок будет немного гибким.

Vignesh 17.05.2021 15:50
Ответ принят как подходящий

Нашел рабочее решение: я проверяю текущее смещение, используя withLatestFrom перед оператором scan, и при необходимости сбрасываю аккумулятор на основе этого значения.

Стекблиц демо

Это действительно очень лаконичное и чистое решение.

h0b0 19.08.2021 09:02

Чтобы управлять 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)),
  ..
)

Другие вопросы по теме