Модуль 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 }) => …),
);
Він не видасть жодного значення, поки кожне джерело не надішле хоча б одне.
Якщо один із потоків — це valueChanges поля вводу, який мовчить до першого набору,
екран лишиться порожнім назавжди. Лікується startWith() на кожному такому
джерелі — і саме тому в попередньому уроці він стоїть у прикладі.
forkJoin — дочекатись усіх і завершити
import { forkJoin } from 'rxjs';
// стартове завантаження екрана: усі довідники одразу
forkJoin({
tasks: this.api.getTasks(),
users: this.api.getUsers(),
statuses: this.api.getStatuses(),
}).subscribe(({ tasks, users, statuses }) => …);
combineLatest | forkJoin | |
|---|---|---|
| Коли видає значення | Щоразу, коли будь-яке джерело змінилось | Один раз, коли всі завершились |
| Що віддає | Останні значення кожного | Останні значення кожного |
| Потребує завершення джерел | Ні | Так — інакше не спрацює ніколи |
| Якщо одне впало | Уся комбінація впала | Уся комбінація впала, решта результатів втрачена |
| Типовий випадок | Фільтри, реактивний 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.