Необычный RxJS

Очередная статья - очередная кошечка Стамбула. В этот раз - необычная =)

Очередная статья — очередная кошечка Стамбула. В этот раз — необычная =)

Всем привет! А вы знали, что RxJS содержит в себе более 100 операторов? Но если ваш проект использует эту библиотеку — скорее всего вы с трудом насчитаете у себя больше пары десятков. Интересная ситуация, да? Не знаю почему так получается, но сегодня я хочу поделиться реальными примерами использования «редких» операторов. Приступаем!

Ты меня игнорируешь?

Наш первый оператор называется ignoreElements. Он делает буквально следующее: пропускает (игнорирует) все элементы стрима. Другими словами, после ignoreElements коллбэк next никогда не срабатывает, сработают только error или complete. Посмотрим пример.

Допустим у нас есть сервер, который нам нужно перезагрузить. Статус сервера может быть online, offline, restart или error:

export enum ServiceStatus {
 online = 'online',
 offline = 'offline',
 restart = 'restart',
 restart = 'error',
}

Метод getServerStatus получает статус сервера, скажем, из NgRx стора. Нам нужно дождаться, когда сервер будет снова online и показать сообщение об успешной перезагрузке.

waitForRestart = (): Observable =>
 this.getServerStatus().pipe(
   filter((status) => status === ServiceStatus.online),
   take(1),
   ignoreElements()
 );

Заметим что waitForRestart имеет тип Observable. Он не пропустит ни одно значение. Мы отфильтровываем только online статус, и после его получения завершаем стрим при помощи take (1).

Далее нам нужно обработать статус error. В этом случае завершим стрим ошибкой:

private processErrorStatus = switchMap((status: ServiceStatus) =>
  status === ServiceStatus.error
    ? throwError(new Error('Error occurred during restart'))
    : of(status),
);

Собираем все вместе:

waitForRestart = (): Observable =>
 this.getServerStatus().pipe(
   this.processErrorStatus,
   filter((status) => status === ServiceStatus.online),
   take(1),
   ignoreElements()
 );

Воспользуемся получившейся функцией:

private showMessageAfterRestart = () =>
 this.waitForRestart().subscribe({
   complete: () => this.showSuccessMessage(), // сообщаем об успехе
   error: () => this.showErrorMessage(), // уведомляем об ошибке
 });

Итак, мы создали чистую и простую функцию waitForRestart, которую легко использовать. Еще раз подчеркну — мы пользуемся только полями complete и error. Next не вернет ни одного сообщения благодаря ignoreElements.

Каждой твари по паре

Следующий оператор — pairwise. Он используется чаще, чем ignoreElements, но все же не очень часто. Начиная со второго значения стрима, pairwise посылает 2 значения — текущее и предыдущее. Это может быть полезно в любой ситуации, когда нужно отслеживать, что предшествовало текущему значению. А теперь, как обычно, рассмотрим реальный пример.

Давайте скажем, что у нас есть список нотификаций, которые появляются в левом верхнем углу страницы, когда в приложении возникает какое-то событие. Все события случаются по очереди. Дана задача : когда у нас есть больше одного события — нужно показать предыдущее, которое «уходит» под текущим. Такой чисто визуальный эффект.

Кстати, потом можно добавить анимации перехода и будет совсем красиво. Но сейчас не об этом, давайте посмотрим как достать последние 2 нотификации из потока.

currentAnnouncement$ = this.notificationsService.getLatest();  // текущая

previousAnnouncement$ = this.currentAnnouncement$.pipe( // предыдущая
 pairwise(),
 map(([previous, current]) => previous)
);

Сначала мы получаем текущую нотификацию. Далее применяем к ней pairwise и получаем уже по 2 элемента в стриме. Осталось только применить оператор map чтобы выбрать предыдущий элемент.

Кто сменил ключи?!

Оператор distinctUntilKeyChanged очень похож на всем знакомый distinctUntilChanged. Но в случае, когда нужно различать измененные элементы по какому-то ключу (например по id), он будет выглядеть в вашем коде более элегантно. Посмотрим пример и сразу всё станет понятно.

Допустим нам в стрим приходят разные пользователи (давайте для простоты  используем оператор of):

users$ = of(
 { age: 22, name: 'Ivan', id: 1 },
 { age: 22, name: 'Ivan', id: 1 },
 { age: 27, name: 'Irina', id: 2 },
 { age: 24, name: 'Aleksey', id: 3 }
);

Ставим задачу: нужно отфильтровать пользователей так, чтобы не было повторений 2 раза подряд.

Что это за пример такой?

Внимательный читатель заметит, что данный пример более абстрактный, чем остальные. Это так. Однако я посчитал, что для столь простого случая не стоит «разжевывать» и описывать бизнес-кейс более подробно.

Пользователь Ivan с Id 1 повторяется 2 раза подряд. Чтобы получать только уникальные значения, воспользуемся distinctUntilChanged:

this.users$.pipe(
  distinctUntilChanged() // не сработает!
);

Не сработало! Поскольку имеем дело с объектами, просто использовать оператор без параметров не получится. Причина в том, что сущности, с которыми мы работаем, являются разными объектами и здесь их нельзя сравнивать по значению. Исправляемся:

this.users$.pipe(
  distinctUntilChanged((a, b) => a.id === b.id)
);

Теперь мы передаем функцию, которая сравнивает пользователей по id. Но хотелось бы эту запись упростить. И тут в игру вступает distinctUntilKeyChanged:

this.users$.pipe(
  distinctUntilKeyChanged('id')
);

Получилось короче и читается легче.

Возьми с него пример

Последний на сегодня оператор называется sample. Чтобы лучше понять его работу, посмотрим официальную схему:

взято с https://rxjs.dev/api/operators/sample

Оператор принимает два стрима, а дальше выпускает последнее значение первого стрима в тот момент, когда второй стрим выдает значение. Как видно на схеме, получившийся стрим выпускает a, затем c, затем d. Он пропускает значения только тогда, когда сработал второй стрим. При этом, если с момента последнего срабатывания новое значение в первый стрим еще не поступило, ничего не происходит. Также на примере видно, что b не пропускается, потому что между b и c во второй стрим ничего не поступило.

Вроде разобрались, но где же можно использовать такое на практике? Допустим, у нас есть слайдер, который позволяет ввести результат от 1 до 10, и кнопка сохранения:

a8cf6ae8b1fcffa2157e3566ca3c9910.png

По клику на кнопку «Save» отправляем новое значение на сервер. При этом мы хотим, чтобы при нескольких кликах значение отсылалось только единожды, пока пользователь его не изменит. И тут нам поможет sample:

sliderChange$: Observable = this.slider.getChanges();  // изменения слайдера

save$ = new Subject();  // используем для кликов

saveSliderChangesOnClick = () => this.sliderChange$.pipe(
  sample(this.save$),
  distinctUntilChanged(),
  switchMap((value) => this.api.saveSliderValue(value)),
  takeUntil(this.componentDestroyed$)
).subscribe();

Итак, sliderChange$ — это в нашей терминологии первый стрим. Он комбинируется с save$ (второй стрим) при помощи sample. Далее переключаемся на функцию сохранения при помощи switchMap и готово! Оператор takeUntil завершает стрим, обычно для этого используют событие уничтожения компонента.

distinctUntilChanged нужен для случая, когда пользователь изменит значение, а потом передумает и выставит старое. Например:

  • Последнее сохраненное значение 1

  • Меняем на 5

  • Передумали, меняем обратно на 1

  • Сохраняем

В результате таких действий на сервер ничего не отправится благодаря distinctUntilChanged.

Чтобы довести дело до конца, закодируем кнопку «Save» (пример для Angular, но тут мог бы быть любой фреймворк):

Вот и всё: sample позволил нам написать довольно удобную логику очень простым способом.

Заключение

Спасибо всем кто дочитал до конца! А вы считаете перечисленные мной операторы редкими? Какими редкими операторами вы пользуетесь на практике?

© Habrahabr.ru