Реактивні системи: чи можна відстежувати залежність в асинхронному коді?

У світі фронтенд-розробки реактивність давно стала стандартом. Інструменти на кшталт React, Vue чи SolidJS дозволяють автоматично оновлювати інтерфейс користувача, коли змінюється стан. Але що робити, якщо наші залежності — не просто змінні, а асинхронні процеси, обіцянки (Promises) чи потоки подій? Чи можливо побудувати справді реактивну систему, яка ефективно працює з асинхронними даними?

Класична реактивність та її межі

У типовій реактивній моделі ми маємо систему залежностей:

  • спостережувані змінні (observables);
  • залежні ефекти (computed або effects);
  • механізм трекінгу (dependency tracking).

У синхронному коді це працює ідеально. Змінивши count, ми автоматично оновлюємо doubleCount. Проте, як тільки в систему потрапляє async/await або fetch, починаються складнощі.

const count = signal(1);
const doubleCount = computed(() => count() * 2); // класично, без асинхронщини

Проблема з асинхронними залежностями

Уявімо:

const userId = signal(1);
const userData = computed(async () => {
  const response = await fetch(`/api/user/${userId()}`);
  return await response.json();
});

Цей код виглядає логічно, але більшість реактивних систем (наприклад, Solid або Vue) не вміють відстежувати залежності всередині async. Як результат, зміна userId не викличе повторне виконання computed.

Як обійти обмеження?

1. Виносити асинхронність за межі computed

const userId = signal(1);
const userData = signal(null);

createEffect(() => {
  fetch(`/api/user/${userId()}`)
    .then(res => res.json())
    .then(data => userData.set(data));
});

Це класичний патерн, який дозволяє “реактивно” працювати з асинхронними джерелами. Але він потребує ручного керування станом, що ускладнює підтримку.

2. Використання бібліотек з підтримкою асинхронної реактивності

RxJS

RxJS дозволяє створювати реактивні стріми, які чудово працюють з асинхронщиною:

const userId$ = new BehaviorSubject(1);
const userData$ = userId$.pipe(
  switchMap(id => from(fetch(`/api/user/${id}`).then(res => res.json())))
);

React Query / TanStack Query

Ці бібліотеки не є “реактивними” у класичному розумінні, але імітують реактивну поведінку через кеш та автоматичне оновлення:

const { data, isLoading } = useQuery(['user', userId], () => fetchUser(userId));

3. Нові підходи: Signals + Async

Деякі сучасні фреймворки, як SolidJS або Qwik, уже експериментують з асинхронною реактивністю. Ідея — дозволити computed або effect працювати з Promise, контролюючи їхнє скасування та повторне виконання.

Проблема скасування

Асинхронні обчислення потрібно скасовувати, якщо залежність змінилася до завершення запиту. Більшість рішень реалізують це вручну або через AbortController.

createEffect(() => {
  const controller = new AbortController();
  fetch(`/api/user/${userId()}`, { signal: controller.signal })
    .then(...)
  return () => controller.abort();
});

Висновок

Реактивність і асинхронність — це два потужні інструменти. Але їх поєднання вимагає обережності. Якщо ваша система не підтримує async всередині computed — шукайте альтернативи: RxJS, React Query або кастомні рішення з signals і effects. Майбутнє реактивних систем однозначно включатиме глибшу підтримку асинхронних сценаріїв — і саме зараз чудовий час, щоб почати їх вивчати.