Реактивні системи: чи можна відстежувати залежність в асинхронному коді?
У світі фронтенд-розробки реактивність давно стала стандартом. Інструменти на кшталт 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
. Майбутнє реактивних систем однозначно включатиме глибшу підтримку асинхронних сценаріїв — і саме зараз чудовий час, щоб почати їх вивчати.