Реактивне програмування — знахідка для лінивих розробників
Всім привіт! Мене звати Дмитро, я займаюсь фронтенд-розробкою, розбираюсь у сучасних вебтехнологіях та не маю симпатії до тестерів. Я студент, якимось чином знаходжу час на навчання, спорт, фронтенд та навіть можу погуляти сходити :) Сьогодні ми поговоримо про парадигму реактивного програмування, її плюси та мінуси, та чому вона для лінивих.
Чи бувало таке, що ви знайшли геніальне, неперевершене рішення проблеми в коді, все працює, але хтось з команди запитав: «А що, якщо зміниться цей параметр?» І раптом усе, що здавалося простим, перетворилося на клубок проблем, які потребують негайного розплутування.
Саме тут на допомогу приходить реактивне програмування — парадигма (підхід або «стиль» написання програм), яка дозволяє не просто спостерігати за змінами, а й автоматично реагувати на них.
Що таке реактивне програмування
Тут одразу зауваження. Якщо у вас немає розуміння, як працює асинхронність в JavaScript, раджу вам прочитати документацію на MDN (Introducing async JavaScript). Якщо з цим усе ок, то...
Тоді почнемо з нудної теорії :)
Реактивне програмування — це парадигма програмування, побудована на потоках даних і розповсюдженні змін. Це означає, що у мовах програмування має бути можливість легко виразити статичні чи динамічні потоки даних, а реалізована модель виконання буде автоматично розсилати зміни через потік даних.
Зрозуміли щось? Давайте простіше...
Уявіть, у вас є складний застосунок із великою кількістю взаємозалежних даних. Замість того, щоб вручну відстежувати всі зміни, ви створюєте систему, де будь-яка зміна автоматично «протікає» через усе дерево залежностей, оновлюючи відповідні компоненти. Це і є реактивне програмування.
Основна ідея: ви визначаєте, на що потрібно реагувати, а все інше система бере на себе. Легко! Поїхали далі!
Які переваги реактивного програмування
А переваг тут і справді багато:
- Простота обробки асинхронності. Робота з асинхронністю в JS може тягнути за собою: головну біль, стрес, перевтому та депресію. На допомогу приходить реактивне програмування, в якому все набагато простіше: ми визначаємо «потік» (stream), який реагує на події, і код сам по собі буде оновлюватися, коли з’являться нові дані. Просто. Без нервів. І без краплі зайвого коду.
- Менше коду для керування станом. Не думаю, що хтось бажає керувати станом вручну. Це як посеред ночі прокидатися та перевіряти, чи заряджається телефон. Замість того, щоб вручну оновлювати значення в коді, реактивне програмування робить це автоматично. Ви лише визначаєте залежності (які елементи залежать від яких), а решта — за реактивною системою. Це економить години коду, і ви не будете повторюватися, як поганий фільм.
- Масштабованість. Коли твій маленький пет-проєкт переходить у великий web-застосунок, він стає більшим і вже не є таким зрозумілим, як був спочатку. З’являється більше функцій, логіки, залежностей, за якими потрібно слідкувати. Реактивні системи автоматично обробляють ці залежності, і ви не витрачаєте час на постійне оновлення різних частин програми. Вони працюють завжди ідеально, незважаючи на масштаби проєкту, це дуже круто.
Головні елементи реактивного програмування
- Потоки даних (Streams). Це, по суті, асинхронні канали, через які ваша програма отримує та обробляє інформацію. Вони дозволяють робити це, не чекаючи на всі дані відразу, а працюючи з ними поступово, як тільки вони з’являються.
- Спостерігачі (Observers). Це особливі об’єкти, які слідкують за всім, що відбувається у потоці даних. Їхня головна мета — бути готовими миттєво реагувати, коли щось змінюється. Це як коли наприкінці місяця перевіряєш картку на зп :)
Тут уважно! Після створення нашого стріма (через об’єкт Observable) потрібно обов’язково підписатись (subscribe). Без підписки стрім не буде працювати й ви опинитесь у вічному очікуванні.
- Оператори (Operators). Це інструменти, які перетворюють звичайну інформацію в щось корисне. Наприклад, ви отримали список чисел, а за допомогою
map
можете помножити всі числа на два, або ж зfilter
відібрати лише парні.
Тепер подивимось, як це все можна реалізувати!
Як це працює
Візьмемо конкретний приклад: у вас є застосунок для онлайн-купівлі квитків. Користувач вибирає місто, і вам потрібно оновити список доступних рейсів. Якщо все робити традиційно, доведеться вручну обробляти всі події, зберігати стан, і, зрештою, переробити рендер. В реактивному ж підході вам достатньо підписатися на потік зміни міста, і список рейсів оновиться автоматично!
1. Створення потоку даних
const selectedCity$ = new BehaviorSubject('Kyiv');
BehaviorSubject
— це спеціальний тип потоку з бібліотеки RxJS. Він зберігає поточний стан (у нашому випадку обране місто) і дозволяє всім, хто підписаний на цей потік, отримувати актуальне значення.
2. Обробка змін у потоці
selectedCity$ .pipe( switchMap(city => fetchFlightsForCity(city)) )
Що тут відбувається?
pipe
— це метод, який дозволяє застосувати декілька операторів до потоку даних, оператори своєю чергою трансформують або обробляють дані. switchMap
— це оператор, який:
- Отримує поточне значення з потоку (
city
). - Викликає функцію
fetchFlightsForCity(city)
, яка надсилає HTTP-запит для отримання рейсів цього міста. - Якщо потік змінюється (користувач обрав інше місто),
switchMap
автоматично скасовує попередній запит і запускає новий.
3. Підписка на потік
.subscribe(flights => updateUI(flights));
subscribe — це метод, який «активує» потік і виконує певну дію з отриманими даними.
У цьому випадку ми отримуємо список рейсів (flights)
і передаємо його у функцію updateUI
, яка оновлює інтерфейс користувача.
Думаю, тут не все так тяжко, все пізнається через документацію ;)
Рухаємось далі...
Обробка помилок у реактивному програмуванні
Мабуть, у вас з’явилося питання: «Діма, а що в цій параштуці робити з помилками, як обробляти?» Зараз все розповім.
У реактивному підході помилки — це не «кінець світу», а просто ще одна частина потоку. Система, яку ми побудуємо, дозволить грамотно їх обробити, щоб застосунок залишався стабільним навіть у критичних ситуаціях.
Нижче — кілька корисних підходів, які рятують від головного болю.
- Перехоплення помилок (catchError). Це як мозок реактивного потоку. Цей оператор сам вирішить, що робити з помилкою: логнути її, повідомити юзера, або, може, перезапустити процес.
import { of, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; const data$ = throwError(() => new Error('Щось пішло не так!')).pipe( catchError(error => { // Додаємо мозок до проекту :) console.error(error.message); // Логуємо помилку return of('Резервні дані'); // Повертаємо резервне дані }) ); data$.subscribe(value => console.log(value));
- Резервні дані (fallback). Теж класний оператор. Якщо система тупить, вона може повернути щось альтернативне. Наприклад, кешовані дані або повідомлення про технічні роботи.
import { of, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; const fetchData$ = throwError(() => new Error('Сервер недоступний')).pipe( catchError(() => of('Дані з кешу')) // Повертаємо резервні дані ); fetchData$.subscribe(value => console.log(value));
- Повторення спроб (retry). Гарного провайдера тяжко обрати, а коли ще з відключеннями світла потрібно бігати до кав’ярні по не найкращий інет, взагалі жах.
import { of, throwError } from 'rxjs'; import { retry, catchError } from 'rxjs/operators';const unstableRequest$ = throwError(() => new Error('Проблема з мережею')).pipe( retry(3), // Повторюємо запит 3 рази catchError(error => of(`Запит провалено: ${error.message}`)) ); unstableRequest$.subscribe(value => console.log(value));
Тому ми видихаємо та використовуємо оператор retry, але без фанатизму.
- Відновлення (resume). У деяких випадках можна продовжити виконання потоку, навіть якщо сталася помилка. Це забезпечує гнучкість і дозволяє обробляти лише критичні помилки, не перериваючи роботу всього застосунку.
import { of, from } from 'rxjs'; import { catchError, map } from 'rxjs/operators';const tasks$ = from([1, 2, 'помилка', 4]).pipe( map(task => { if (task === 'помилка'){ throw new Error('Помилка у потоці!');} return task * 2; }), catchError(() => of('Продовжуємо роботу'))); tasks$.subscribe(value => console.log(value));
У разі помилки ми не кидаємо всю роботу, а акуратно обробляємо проблему і продовжуємо.
Основні інструменти
Реактивне програмування застосовується в багатьох мовах програмування, але ми ж фронтендери, то й хотілось би дізнатися, як та за допомогою яких інструментів ми можемо робити код краще.
Інструментів багато, є більш нові, є більш зручні, але я виділю, мабуть, найкращі:
1. RxJS
RxJS — це клондайк для розробників, які хочуть використовувати всі переваги реактивного програмування. Ця бібліотека створена для управління потоками подій, асинхронних операцій і будь-яких змін у даних у реальному часі.
RxJS має величезний набір операторів (map
, filter
, reduce
, merge
), які дозволяють легко трансформувати та комбінувати дані.
Розглянемо кейс. Уявіть, є поле пошуку, є проблема — в компанії не дуже сильні сервера. Що робити? Ми можемо щось вигадати на звичайному JS, наприклад:
const searchInput = document.getElementById('search'); searchInput.addEventListener('input', () => { setTimeout(() => { // Затримка 300 мс console.log(searchInput.value); }, 300); });
Воно буде працювати, але ми задовбаємось контролювати затримку вручну та скасовувати зайві виклики.
Тепер реалізуємо те ж саме з RxJS:
// Імпортуємо необхідні функції з бібліотеки RxJS import { fromEvent } from 'rxjs'; // Створення потоку подій з DOM-елемента import { debounceTime, map } from 'rxjs/operators'; // Оператори для роботи з потоками // Знаходимо елемент поля вводу за його ID const searchInput = document.getElementById('search'); // Створюємо поток подій для введення тексту у поле fromEvent(searchInput, 'input') // Слухаємо події "input" на елементі .pipe( debounceTime(300), // Робимо затримку 300 мс, щоб уникнути частих викликів map(event => event.target.value) // Отримуємо значення тексту з події ) .subscribe(value => console.log(value)); // Виводимо значення
Супер, сервера мають надію на життя і тепер бек буде приносити вам ранкову каву :)
2. React + Redux-Observable
React хоч крутий фреймворк, але не є повністю реактивним (гарно написано). Його можна вдосконалити за допомогою інструментів, як-от Redux-Observable. Цей middleware для Redux дозволяє обробляти асинхронні дії у вигляді потоків, що дає змогу реалізувати складну бізнес-логіку.
Приклад. Уявімо, що ваша апка має список користувачів, який завантажується при відкритті сторінки. При цьому можуть бути затримки через сервер, помилки мережі чи відсутність даних, ситуація базова.
Якщо без Redux-Observable:
const fetchUser = async () => { try { const response = await fetch('https://jsonplaceholder.typicode.com/users/1'); const data = await response.json(); console.log(data); } catch (error) { console.error('Помилка:', error); } }; fetchUser();
Вручну обробляти помилки, слідкувати за запитами, мені це ліньки робити :)
Для лінивих Redux-Observable вирішує це так:
import { ofType } from 'redux-observable'; // Фільтрує потік дій за їх типом import { switchMap, catchError, map } from 'rxjs/operators'; // Оператори для обробки потоків import { ajax } from 'rxjs/ajax'; // Використовується для виконання HTTP-запитів import { of } from 'rxjs'; // Створює нові потоки даних const fetchUserEpic = (action$) => action$.pipe( // Перевіряємо тип вхідної дії, залишаємо тільки дії з типом 'FETCH_USER' ofType('FETCH_USER'), // Виконуємо HTTP-запит замість вхідної дії switchMap(() => ajax.getJSON('https://jsonplaceholder.typicode.com/users/1').pipe( // Якщо запит успішний, створюємо новий екшен 'FETCH_USER_SUCCESS' map((response) => ({ type: 'FETCH_USER_SUCCESS', // Тип нової дії payload: response, // Дані, отримані із запиту })), // Якщо виникла помилка, створюємо новий екшен 'FETCH_USER_ERROR' catchError((error) => of({ type: 'FETCH_USER_ERROR', // Тип дії для помилки payload: error.message, // Текст помилки додається у payload }) ) ) ) ); export default fetchUserEpic;
Ну, виглядає зашибісь і працює також гарно.
3. Vue.js (реактивність «із коробки»)
Як мені цього не хотілось, але мушу сказати, що тут Vue крутіше за React :(
Vue вже побудований на реактивній моделі, яка дозволяє автоматично оновлювати DOM при зміні стану. Це значить, що більше ніякого ручного оновлення — Vue все зробить за вас.
<template> <div> <p>Значення лічильника: {{ count }}</p> <button @click="increment">Додати</button> </div> </template> <script> export default { data() { return { count: 0 // Реактивна змінна }; }, methods: { increment() { this.count++; // Автоматично оновлює DOM } } }; </script>
Коли ви створюєте змінні у data()
або використовуєте ref
у Composition API, Vue автоматично відстежує ці змінні, і це не може не вабити.
На JS це було б так:
const countElement = document.getElementById('count'); const button = document.getElementById('increment'); let count = 0; button.addEventListener('click', () => { count++; countElement.textContent = `Значення лічильника: ${count}`; });
Реактивне програмування для всіх?
Всі ми знаємо, як початківці, побачивши нову технологію або інструмент для розробки та прочитавши дві статті, вважають себе синьор реакт фулстек фронтенд бекенд розробниками. Але парадигма реактивного програмування — це трошки важче, ніж здається.
Розповім про проблеми, які можуть виникнути, та як я їх розв’язував:
- Крива навчання. Коли читав документацію, гортав гугл та думав, куди ж цю парадигму засунути, часом з’явилося велике бажання викинути ноут через кватирку :) Тому будьте готові, що не одразу зрозумієте, навіщо це вам. Почніть із простих прикладів, типу обробки кліків чи запитів до API, всі ми починали з малого. З часом ви зрозумієте, що реактивщина — це не так страшно, як здається.
- Дебагінг. Тут починається жестяк. У звичайному коді помилку ми знаходимо через
console.log
. Але коли потік проходить через 10 операторів, а на виході виходить «undefined», починаєш сумувати за часами, коли ти просто верстав звичайні лендінги. Щоб голова не закипіла та життя засяяло кольорами, спробуйте розбити складний потік на менші шматочки. Краще мати 5 маленьких функцій, ніж одну, яку тільки зрозумієш і то завтра забудеш.
💡 Перша конференція DOU Front-end Day 2025 - Зловживання операторами. Кожен розробник бажає, щоб його код був найкращим. Побачивши нові технології, думає: «А чому б не вставити їх сюди, тут і ще отам?» Але через місяць код перетворюється на незрозумілий хаос, який важко читати, а розуміти взагалі неможливо.
- Ваш код — купа незрозуміло чого. Ви повинні писати його за таким принципом: «Код читається частіше, ніж пишеться». Ваш колега (або ви самі через три тижні) має зрозуміти, що відбувається. Якщо питання вирішується звичайним
map
, не треба притягувати switchMap тільки тому, що він звучить крутіше.

Підсумок
Підсумок простий: реактивність дозволяє вам менше перейматися і більше створювати. Якщо ще не спробували, обов’язково дайте цьому шанс. Можливо, це саме те, чого бракувало вашому коду.
Також я веду Телеграм-канал, де ви знайдете багато цікавого про JavaScript, фронтенд, корисні інструменти, практичні задачі та кар’єрні поради. Підписуйтесь! 🚀
33 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів