Angular IDPStrong Junior
Модулі / Дані, моки й RxJS / Комбінування потоків
🔥 0

Модуль 4 · урок 5 з 13

Комбінування потоків

Комбінування — це те, заради чого RxJS узагалі варто вчити. Коли на екрані три фільтри, пошук і дані з двох джерел, звичайним кодом виходить каша з прапорців. Одним combineLatest — три рядки.

combineLatest — щоразу, коли щось змінилось

import { combineLatest } from 'rxjs';

readonly visible$ = combineLatest([
  this.tasks$,
  this.status$,
  this.query$,
]).pipe(
  map(([tasks, status, query]) => tasks
    .filter(t => status === 'all' || t.status === status)
    .filter(t => !query || t.title.toLowerCase().includes(query))
  ),
);

Або в об'єктній формі — читається краще, коли джерел більше трьох:

combineLatest({
  tasks: this.tasks$,
  status: this.status$,
  query: this.query$,
}).pipe(
  map(({ tasks, status, query }) => …),
);
Головна пастка combineLatest

Він не видасть жодного значення, поки кожне джерело не надішле хоча б одне. Якщо один із потоків — це valueChanges поля вводу, який мовчить до першого набору, екран лишиться порожнім назавжди. Лікується startWith() на кожному такому джерелі — і саме тому в попередньому уроці він стоїть у прикладі.

forkJoin — дочекатись усіх і завершити

import { forkJoin } from 'rxjs';

// стартове завантаження екрана: усі довідники одразу
forkJoin({
  tasks: this.api.getTasks(),
  users: this.api.getUsers(),
  statuses: this.api.getStatuses(),
}).subscribe(({ tasks, users, statuses }) => …);
combineLatestforkJoin
Коли видає значенняЩоразу, коли будь-яке джерело змінилосьОдин раз, коли всі завершились
Що віддаєОстанні значення кожногоОстанні значення кожного
Потребує завершення джерелНіТак — інакше не спрацює ніколи
Якщо одне впалоУся комбінація впалаУся комбінація впала, решта результатів втрачена
Типовий випадокФільтри, реактивний UIСтартове завантаження

Найчастіша помилка з forkJoin: дати йому потік, який не завершується — наприклад, BehaviorSubject або valueChanges. Тоді підписка просто мовчить, і зрозуміти чому важко. HTTP-запити завершуються самі, тому з ними він працює ідеально.

withLatestFrom — головний потік і довідка

import { withLatestFrom } from 'rxjs';

// зберігаємо по кліку, підмішуючи поточні фільтри — але сам фільтр збереження не запускає
saveClicks$.pipe(
  withLatestFrom(this.filters$),
  switchMap(([, filters]) => this.api.save(filters)),
);

Різниця з combineLatest принципова: тут є ведучий потік. Зміна filters$ нічого не запускає — вона лише оновлює те значення, яке буде підмішане при наступному кліку.

merge, zip, race

import { merge, zip, race } from 'rxjs';

// merge — усе в один потік, хто перший, того й тапки
merge(this.created$, this.updated$, this.deleted$).subscribe(() => this.reload());

// zip — по парах: перше з першим, друге з другим (використовується рідко)
zip(this.ids$, this.names$);

// race — переможець перший, решта скасовується
race(this.fromCache$, this.fromNetwork$);

Реальний приклад: екран списку цілком

export class TaskStore {
  private readonly api = inject(TaskApiService);

  readonly status$ = new BehaviorSubject<TaskStatus | 'all'>('all');
  readonly query$ = new BehaviorSubject('');
  readonly page$ = new BehaviorSubject(1);

  private readonly all$ = this.api.getTasks().pipe(
    shareReplay({ bufferSize: 1, refCount: true }),
  );

  readonly visible$ = combineLatest({
    tasks: this.all$,
    status: this.status$,
    query: this.query$.pipe(debounceTime(300), distinctUntilChanged()),
    page: this.page$,
  }).pipe(
    map(({ tasks, status, query, page }) => {
      const filtered = tasks
        .filter(t => status === 'all' || t.status === status)
        .filter(t => !query || t.title.toLowerCase().includes(query.toLowerCase()));

      return {
        rows: filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
        total: filtered.length,
      };
    }),
  );
}

Це майже готовий екран із модуля 5. Зверни увагу: жодного прапорця, жодного «якщо фільтр змінився, перерахувати». Опис залежностей — і все перераховується саме тоді, коли треба.

Як обирати
  • Реактивний UI із кількох джерел — combineLatest.
  • Стартове завантаження кількох запитів — forkJoin.
  • Дія плюс поточний контекст — withLatestFrom.
  • Кілька подій, реакція одна — merge.
Типові помилки
  • forkJoin із потоком, що не завершується.
  • combineLatest без startWith — тиша замість даних.
  • combineLatest там, де потрібен ведучий потік.
  • Вкладені підписки замість комбінування — про це в уроці 10.