Внедрение зависимостей и RxJS — Примеры

Пример 1 — Поиск, сохраняющий запрос на каждое нажатие

Поле поиска отображает свой ввод в запрос, и результаты иногда принадлежат более раннему запросу. Скажите, какой оператор оставляет старые запросы живыми, и отмените их.

readonly results$ = this.query$.pipe(
  debounceTime(250),
  mergeMap((q) => this.http.get<Result[]>(`/api/search?q=${encodeURIComponent(q)}`)),
);

Решение

readonly results$ = this.query$.pipe(
  debounceTime(250),
  distinctUntilChanged(),
  switchMap((q) => this.http.get<Result[]>(`/api/search?q=${encodeURIComponent(q)}`)),
  catchError(() => of([])),
);

Observable — не промис, а поток: промис разрешается один раз, а observable может испустить ноль, одно или много значений. Операторы описывают, как один поток значений становится другим: mergeMap оставляет работать каждый внутренний observable, поэтому медленный ранний запрос остаётся жив и может испустить значение после более быстрого позднего. switchMap отписывается от предыдущего внутреннего observable, когда приходит новое значение, и это ровно та отмена, которая нужна полю поиска; именно поэтому результаты всегда принадлежат текущему запросу. debounceTime вообще сокращает число запускаемых запросов, а distinctUntilChanged не даёт повторному значению запустить ту же работу заново. Читайте цепочку операторов слева направо как путь, который проходит значение, — так конвейеры и становятся понятными.

Пример 2 — Подписка, переживающая свой компонент

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

export class OrdersComponent implements OnInit {
  orders: Order[] = [];

  constructor(private readonly ordersService: OrdersService) {
    this.ordersService.list().subscribe((orders) => (this.orders = orders));
  }
}

Решение

export class OrdersComponent {
  readonly orders$ = inject(OrdersService).list();
}
@if (orders$ | async; as orders) {
  @for (order of orders; track order.id) { <li>{{ order.id }}</li> }
}

Ручная подписка держит ссылку на компонент столько же, сколько живёт поток, поэтому каждый визит на экран добавляет ещё одного слушателя, а освободить компонент нельзя: симптом — устаревшие данные, приходящие после того, как пользователь уже ушёл. Пайп async подписывается, когда шаблон отрисовывается, и отписывается, когда его уничтожают, что полностью убирает бухгалтерию в ngOnDestroy, а там, где подписка обязана быть ручной, ту же работу делает takeUntilDestroyed. Подписка в конструкторе — вторая ошибка, которую нужно заметить: она выполняется до инициализации компонента и до того, как задан хоть один вход, поэтому работа, которая должна произойти один раз, принадлежит ngOnInit, а её очистка — ngOnDestroy.