Реактивне програмування

Реактивне програмування

  • 14 березня
  • читати 20 хв
Володимир Шайтан
Володимир Шайтан Senior Full Stack Developer у UKEESS Software House, Викладач Комп'ютерної школи Hillel.
Дмитро Солтановський
Дмитро Солтановський Junior Frontend-developer | JavaScript | React

Було таке, що ви знаходили геніальне, неперевершене розв'язання проблеми у коді, все працює ідеально, але тут колега питає: «А що, якщо зміниться отой параметр?». І враз виявляється, що навіть найпростіша, на перший погляд, конструкція обертається клубком взаємозалежних задач. Знайомо? Саме тут починає сяяти реактивне програмування.

ЩО ТАКЕ РЕАКТИВНЕ ПРОГРАМУВАННЯ?

Якщо ви час від часу зустрічалися з асинхронним JavaScript, але поки не поглиблювалися у деталі, раджу спершу зазирнути у документацію на MDN ("Introducing async JavaScript"). Якщо з цим усе гаразд, тоді перейдемо до суті.

Реактивне програмування — це парадигма, побудована навколо потоків даних та автоматичного поширення змін.

Якщо висловитись простіше: уявіть собі складний застосунок з цілим деревом залежних даних. Реактивний підхід полягає в тому, що якщо у цьому дереві щось змінюється, то все, що залежить від цієї зміни, «оновлюється» автоматично. Тобто ви лише задаєте умови — а події самі перетікають через усі системні зв’язки. Ніяких нескінченних ручних оновлень.

ОСНОВНА ІДЕЯ

Замість того, щоб вручну відстежувати, де й коли потрібно оновити дані, ви визначаєте «точки реакції». Коли приходить подія (наприклад, користувач змінив якийсь параметр), ця подія розповсюджується по ланцюгу залежностей, а всі потрібні частини застосунку підлаштовуються автоматично. Менше клопоту, менше помилок, більше сконцентрованості на бізнес-логіці.

ЯКІ ПЕРЕВАГИ РЕАКТИВНОГО ПРОГРАМУВАННЯ?

Нумо подивимось на переваги. Звісно, згадаємо про асинхронність, бо це ледь не ключова частина.

  1. Простота обробки асинхронності. Ніхто не любить сотні колбеків, промісів і можливих «витікань». Реактивний підхід дає змогу легко налаштувати потік, який автоматично реагуватиме на надходження нових даних. Код скорочується, а нерви — цілі.
  2. Менше коду для керування станом. Нема потреби вручну оновлювати змінні чи зберігати посилання, щоб відстежити будь-який клік. Якщо в одному місці щось змінилося — реактивна система сама «підхопить» цю подію. Це економить години коду і рятує від дублювання.
  3. Масштабованість. Коли ваш маленький проєкт перетворюється на великий вебзастосунок, кількість залежностей різко зростає. У реактивній системі цей ріст не лякає: що більше у вас змін, то органічніше система «знаходить» куди їх ретранслювати. Продуктивність і зрозумілість залишаються на високому рівні навіть у масштабних рішеннях.

ГОЛОВНІ ЕЛЕМЕНТИ РЕАКТИВНОГО ПРОГРАМУВАННЯ

Щоб ближче зрозуміти, як це працює, варто дізнатися про базові поняття.

  1. Потоки даних (Streams). Уявіть їх як «трубопроводи», через які безперервно протікає інформація. Ви можете «підключитися» до цього потоку, а система подбає про те, щоб ви завжди отримували актуальні дані.
  2. Спостерігачі (Observers). Вони «слухають» потоки й реагують, коли з'являється нова порція інформації. Це як зарплата, яка «прилітає» на картку в неочікуваний момент: достатньо бути підписаним на оновлення, щоб одразу це помітити.
  3. Оператори (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}`))
);

Так ми можемо налаштувати стратегію, яка найбільше підходить під конкретне бізнес-завдання.

ОСНОВНІ ІНСТРУМЕНТИ

Реактивне програмування — це більше, ніж просто концепція. Є безліч бібліотек і фреймворків, які втілюють цю ідею у реальному коді.

  1. RxJS. Найпопулярніша бібліотека для JavaScript, яка надає засоби керування потоками (Observable, Subject і багато операторів). Шалено потужна, широко використовується у фронтенді.
  2. React + Redux-Observable. Якщо вам подобається React і Redux, то Redux-Observable дозволить обробляти складні асинхронні процеси у вигляді «епіків» (epics). Легко масштабувати, легко тестувати.
  3. 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. Це і є один із найпростіших прикладів «реактивності».

РЕАКТИВНЕ ПРОГРАМУВАННЯ ДЛЯ ВСІХ?

Може здатися, що щойно ви побачили «реактивний» підхід, то швидко перейдете на нього у всьому. Та не все так просто.

  1. Крива навчання. Спочатку складно зрозуміти, як «потоки» і «підписки» взаємодіють, особливо якщо є маса операторів типу map, switchMap, mergeMap, concatMap, catchError тощо. Іноді хочеться повернутися до звичайного console.log.
  2. Дебагінг. Якщо ваш потік проходить через 10 операторів і в кінці ви отримуєте undefined, важко зрозуміти, де саме сталось «обрив». Допомагає розбиття складного потоку на кілька простіших етапів.
  3. Зловживання операторами. Спокуса використати все й одразу велика. Але іноді досить однієї функції map замість трьох різних операторів, що ускладнюють код.

Практичні поради

  • Починайте з маленьких прикладів: обробка кліків, API-запитів.
  • Не намагайтеся робити «просто щоб було модно». Вибирайте реактивний підхід там, де є реальна потреба.
  • Створюйте невеликі оператори й елементи потоку, щоб було легше відстежувати ланцюжок виконання.

ПІДСУМОК

Реактивне програмування надає потужні інструменти для роботи зі змінами, особливо коли є багато залежностей і асинхронних процесів. Воно допомагає писати чистіший код, що в майбутньому легко розширювати й підтримувати. Якщо ви досі не наважувалися спробувати — варто дати шанс.

Пам’ятайте, що це не «панацея», і деякі завдання можна вирішити простіше. Але коли ваш застосунок росте, реактивний підхід стає тим містком, що дозволяє зберегти продуктивність і зрозумілість коду. Тож читайте документацію, експериментуйте, і нехай ваш проєкт засяє новими кольорами!

Рекомендуємо публікації по темі