Модуль 4 · урок 3 з 13
Базові оператори
Оператор — це функція, яка бере потік і повертає новий. Вони не змінюють вихідний
потік, а будують над ним нову обгортку. Тому pipe читається як конвеєр: значення
заходить згори й проходить кожен щабель по черзі.
Як читати pipe
source$.pipe(
filter(t => t.status !== 'done'), // 1. пропустити тільки незавершені
map(t => t.title), // 2. взяти лише назву
take(5), // 3. зупинитись після пʼятого
).subscribe(console.log);
Кожне значення проходить весь ланцюжок окремо — не так, що спершу відфільтрувався весь масив, а потім перетворився. Це важливо для розуміння: потік обробляє значення по одному, у міру їх надходження.
Робочий мінімум
import { map, filter, tap, scan, take, first, distinctUntilChanged, startWith } from 'rxjs';
map — перетворити кожне значення
this.http.get<TaskDto[]>('/api/tasks').pipe(
map(dtos => dtos.map(toTask)), // DTO → доменна модель із модуля 2
);
filter — пропустити не все
fromEvent<KeyboardEvent>(input, 'keydown').pipe(
filter(e => e.key === 'Enter'),
);
tap — подивитись, нічого не змінюючи
source$.pipe(
tap(v => console.log('до фільтра:', v)), // дебаг
filter(v => v > 10),
tap({ subscribe: () => this.loading.set(true),
finalize: () => this.loading.set(false) }), // побічні дії
);
tap — єдиний оператор для побічних дій. Якщо всередині map зʼявилось
щось окрім перетворення, це майже завжди має бути tap.
scan — накопичувати
clicks$.pipe(
scan(count => count + 1, 0), // 1, 2, 3, … — як reduce, але віддає кожен крок
);
Різниця з reduce: той чекає завершення потоку й віддає одне підсумкове значення,
а scan віддає проміжний результат після кожного кроку. Для UI майже завжди потрібен
саме scan.
take, first — обмежити
source$.pipe(take(1)); // перше значення й завершити — часта заміна ручному unsubscribe
source$.pipe(first(v => v.ready)); // перше, що підійшло; кине помилку, якщо потік завершився без збігу
distinctUntilChanged — не повторюватись
query$.pipe(
distinctUntilChanged(), // пропускає значення, однакові з попереднім
);
// для обʼєктів — своє порівняння
filters$.pipe(
distinctUntilChanged((a, b) => a.status === b.status && a.query === b.query),
);
startWith — дати початкове значення
this.searchControl.valueChanges.pipe(
startWith(''), // щоб список показався одразу, а не після першого набору
);
Дрібниця, без якої екран лишається порожнім до першої дії користувача. У модулі 5 вона знадобиться для кожного фільтра таблиці.
Типовий ланцюжок з реального коду
readonly visibleTasks$ = this.searchControl.valueChanges.pipe(
startWith(''), // показати щось одразу
map(v => (v ?? '').trim().toLowerCase()),
distinctUntilChanged(), // не смикатись на однакове
debounceTime(300), // урок 6
switchMap(q => this.api.search(q)), // урок 4
);
Прочитай його вголос: «беремо зміни поля, починаємо з порожнього, чистимо рядок,
ігноруємо повтори, чекаємо паузу в наборі, робимо запит». Саме так і треба читати
pipe — як речення.
Порядок операторів має значення. debounceTime перед map і після
нього дадуть однаковий результат, а от take(1) на початку й у кінці — зовсім
різні речі. Загальне правило: спершу відсіювати зайве (filter,
distinctUntilChanged), потім перетворювати, і аж потім робити щось дороге —
запити, підписки на інші потоки.