Angular IDPStrong Junior
Модулі / Дані, моки й RxJS / Базові оператори
🔥 0

Модуль 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), потім перетворювати, і аж потім робити щось дороге — запити, підписки на інші потоки.