Було таке, що ви знаходили геніальне, неперевершене розв'язання проблеми у коді, все працює ідеально, але тут колега питає: «А що, якщо зміниться отой параметр?». І враз виявляється, що навіть найпростіша, на перший погляд, конструкція обертається клубком взаємозалежних задач. Знайомо? Саме тут починає сяяти реактивне програмування.
ЩО ТАКЕ РЕАКТИВНЕ ПРОГРАМУВАННЯ?
Якщо ви час від часу зустрічалися з асинхронним JavaScript, але поки не поглиблювалися у деталі, раджу спершу зазирнути у документацію на MDN ("Introducing async JavaScript"). Якщо з цим усе гаразд, тоді перейдемо до суті.
Реактивне програмування — це парадигма, побудована навколо потоків даних та автоматичного поширення змін.
Якщо висловитись простіше: уявіть собі складний застосунок з цілим деревом залежних даних. Реактивний підхід полягає в тому, що якщо у цьому дереві щось змінюється, то все, що залежить від цієї зміни, «оновлюється» автоматично. Тобто ви лише задаєте умови — а події самі перетікають через усі системні зв’язки. Ніяких нескінченних ручних оновлень.
ОСНОВНА ІДЕЯ
Замість того, щоб вручну відстежувати, де й коли потрібно оновити дані, ви визначаєте «точки реакції». Коли приходить подія (наприклад, користувач змінив якийсь параметр), ця подія розповсюджується по ланцюгу залежностей, а всі потрібні частини застосунку підлаштовуються автоматично. Менше клопоту, менше помилок, більше сконцентрованості на бізнес-логіці.
ЯКІ ПЕРЕВАГИ РЕАКТИВНОГО ПРОГРАМУВАННЯ?
Нумо подивимось на переваги. Звісно, згадаємо про асинхронність, бо це ледь не ключова частина.
- Простота обробки асинхронності. Ніхто не любить сотні колбеків, промісів і можливих «витікань». Реактивний підхід дає змогу легко налаштувати потік, який автоматично реагуватиме на надходження нових даних. Код скорочується, а нерви — цілі.
- Менше коду для керування станом. Нема потреби вручну оновлювати змінні чи зберігати посилання, щоб відстежити будь-який клік. Якщо в одному місці щось змінилося — реактивна система сама «підхопить» цю подію. Це економить години коду і рятує від дублювання.
- Масштабованість. Коли ваш маленький проєкт перетворюється на великий вебзастосунок, кількість залежностей різко зростає. У реактивній системі цей ріст не лякає: що більше у вас змін, то органічніше система «знаходить» куди їх ретранслювати. Продуктивність і зрозумілість залишаються на високому рівні навіть у масштабних рішеннях.
ГОЛОВНІ ЕЛЕМЕНТИ РЕАКТИВНОГО ПРОГРАМУВАННЯ
Щоб ближче зрозуміти, як це працює, варто дізнатися про базові поняття.
- Потоки даних (Streams). Уявіть їх як «трубопроводи», через які безперервно протікає інформація. Ви можете «підключитися» до цього потоку, а система подбає про те, щоб ви завжди отримували актуальні дані.
- Спостерігачі (Observers). Вони «слухають» потоки й реагують, коли з'являється нова порція інформації. Це як зарплата, яка «прилітає» на картку в неочікуваний момент: достатньо бути підписаним на оновлення, щоб одразу це помітити.
- Оператори (Operators). Це невеликі «робітники», які перетворюють дані за певними правилами. Наприклад, filter пропустить тільки ті елементи, які відповідають певним умовам, а map може перевести число у його квадрат або об'єкт в іншу структуру.
Приклад
Уявімо, що в застосунку є список міст, звідки можна купити квитки. Користувач змінює місто, а ви хочете оновити доступні рейси без зайвих перевірок.
// Потік, у якому зберігається поточне місто
const selectedCity$ = new BehaviorSubject('Kyiv');
// Якщо місто змінюється, автоматично відправляємо запит й отримуємо результати
selectedCity$
.pipe(
switchMap(city => fetchFlightsForCity(city))
)
.subscribe(flights => updateUI(flights));
Ви вказали лише початкову точку (selectedCity$) і функцію, яка викликає API. Якщо користувач вибирає інше місто, попередній запит автоматично скасовується, а новий негайно стартує. Неймовірно зручно.
ЯК ЦЕ ПРАЦЮЄ ГЛИБШЕ?
Усе крутиться навколо того, що застосунок сприймає події (зміни) як «сигнали». Як тільки сигнал з'являється, він подорожує потоком і змінює кінцеві точки. Вам не потрібно писати десяток умов у різних місцях — усе централізовано.
Приклад: оновлення UI при зміні міста
Користувач вибирає інше місто. selectedCity$ шле сигнал: «Місто змінено». Система дає команду: «Скасуй старий запит» і «Виконай новий». Коли дані надходять у потік, subscribe підхоплює результат і викликає updateUI, оновлюючи інтерфейс. Тобто ніякого ручного "if cityChanged" — усе на плаву.
Обробка помилок у реактивному підході
Насправді навіть помилки — це теж «подія», яка може передаватися у потоці. Тобто коли трапилася помилка, ви можете вирішити, що робити:
- повернути кешовані дані;
- показати повідомлення користувачу;
- повторити запит декілька разів.
Приклади
// Якщо сервер не відповідає, повертаємо «резервні» дані
const fetchData$ = throwError(() => new Error('Сервер недоступний')).pipe(
catchError(() => of('Дані з кешу'))
);
// Кілька повторних спроб у разі проблем із мережею
const unstableRequest$ = throwError(() => new Error('Проблема з мережею')).pipe(
retry(3),
catchError(error => of(`Запит провалено: ${error.message}`))
);
Так ми можемо налаштувати стратегію, яка найбільше підходить під конкретне бізнес-завдання.
ОСНОВНІ ІНСТРУМЕНТИ
Реактивне програмування — це більше, ніж просто концепція. Є безліч бібліотек і фреймворків, які втілюють цю ідею у реальному коді.
- RxJS. Найпопулярніша бібліотека для JavaScript, яка надає засоби керування потоками (Observable, Subject і багато операторів). Шалено потужна, широко використовується у фронтенді.
- React + Redux-Observable. Якщо вам подобається React і Redux, то Redux-Observable дозволить обробляти складні асинхронні процеси у вигляді «епіків» (epics). Легко масштабувати, легко тестувати.
- Vue.js. Навіть якщо Vue не кличе себе «реактивним фреймворком», під капотом він реалізує реактивну модель: змінили змінну — і DOM автоматично відобразив ці зміни.
RxJS приклад із полем пошуку
import { fromEvent } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';
const searchInput = document.getElementById('search');
fromEvent(searchInput, 'input')
.pipe(
debounceTime(300),
map(event => event.target.value)
)
.subscribe(value => console.log(value));
Замість безлічі таймерів і ручного скасування запиту при кожному натисканні, debounceTime(300) затримує виклик на 300 мс, і ви отримуєте спокійний потік «актуального» тексту.
React + Redux-Observable
Звичайний React не надто «реактивний» з погляду керування потоками, але з Redux-Observable ситуація змінюється.
import { ofType } from 'redux-observable';
import { switchMap, catchError, map } from 'rxjs/operators';
import { ajax } from 'rxjs/ajax';
import { of } from 'rxjs';
const fetchUserEpic = (action$) =>
action$.pipe(
ofType('FETCH_USER'),
switchMap(() =>
ajax.getJSON('https://jsonplaceholder.typicode.com/users/1').pipe(
map((response) => ({
type: 'FETCH_USER_SUCCESS',
payload: response,
})),
catchError((error) =>
of({
type: 'FETCH_USER_ERROR',
payload: error.message,
})
)
)
)
);
У результаті будь-який запит вміло обробляється через епіки, що реагують на події "FETCH_USER" і повертають потік із новими даними.
Vue.js
У Vue все працює «з коробки». Якщо ви оголошуєте змінну у data(), Vue автоматично відстежує її зміни й перерендерює інтерфейс. Наприклад:
<template>
<div>
<p>Значення лічильника: {{ count }}</p>
<button @click="increment">Додати</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
};
},
methods: {
increment() {
this.count++;
}
}
};
</script>
При кожному кліку count збільшується, а Vue автоматично оновлює DOM. Це і є один із найпростіших прикладів «реактивності».
РЕАКТИВНЕ ПРОГРАМУВАННЯ ДЛЯ ВСІХ?
Може здатися, що щойно ви побачили «реактивний» підхід, то швидко перейдете на нього у всьому. Та не все так просто.
- Крива навчання. Спочатку складно зрозуміти, як «потоки» і «підписки» взаємодіють, особливо якщо є маса операторів типу map, switchMap, mergeMap, concatMap, catchError тощо. Іноді хочеться повернутися до звичайного console.log.
- Дебагінг. Якщо ваш потік проходить через 10 операторів і в кінці ви отримуєте undefined, важко зрозуміти, де саме сталось «обрив». Допомагає розбиття складного потоку на кілька простіших етапів.
- Зловживання операторами. Спокуса використати все й одразу велика. Але іноді досить однієї функції map замість трьох різних операторів, що ускладнюють код.
Практичні поради
- Починайте з маленьких прикладів: обробка кліків, API-запитів.
- Не намагайтеся робити «просто щоб було модно». Вибирайте реактивний підхід там, де є реальна потреба.
- Створюйте невеликі оператори й елементи потоку, щоб було легше відстежувати ланцюжок виконання.
ПІДСУМОК
Реактивне програмування надає потужні інструменти для роботи зі змінами, особливо коли є багато залежностей і асинхронних процесів. Воно допомагає писати чистіший код, що в майбутньому легко розширювати й підтримувати. Якщо ви досі не наважувалися спробувати — варто дати шанс.
Пам’ятайте, що це не «панацея», і деякі завдання можна вирішити простіше. Але коли ваш застосунок росте, реактивний підхід стає тим містком, що дозволяє зберегти продуктивність і зрозумілість коду. Тож читайте документацію, експериментуйте, і нехай ваш проєкт засяє новими кольорами!