Перейти к содержимому

19. RxJS vs Effector

Привет! Яша здесь. Два мощных инструмента реактивного программирования — RxJS и Effector. Один — универсальная библиотека потоков, другой — state manager с чётким API. Сравним философию, подходы и когда что выбирать. Спойлер: оба хороши, просто для разных задач.


1. 🧠 Философия: разные подходы к одной проблеме

Заголовок раздела «1. 🧠 Философия: разные подходы к одной проблеме»

RxJS — это библиотека потоков данных. Фокус на операторах, цепочках преобразований, управлении асинхронностью. Думай о трубах и операторах как водопровод: данные текут, преобразуются, комбинируются.

Effector — это state manager с чётко разделёнными концепциями: Event, Store, Effect. Думай о нём как о конечном автомате: события запускают переходы состояний.

// Effector-концепции и их RxJS-аналоги:
// Event (Effector) ≈ Subject (RxJS)
// — триггер, представляет намерение
const buttonClicked = createEvent<void>(); // Effector
const buttonClicked$ = new Subject<void>(); // RxJS
// Store (Effector) ≈ BehaviorSubject (RxJS)
// — хранит текущее значение
const $counter = createStore(0); // Effector
const counter$ = new BehaviorSubject(0); // RxJS
// Effect (Effector) ≈ Subject + mergeMap (RxJS)
// — обёртка над async-операцией с состояниями
const fetchUser = createEffect(async (id: number) => {
return await api.getUser(id); // Effector
});
const fetchUser$ = new Subject<number>();
const userResult$ = fetchUser$.pipe(
mergeMap(id => from(api.getUser(id))) // RxJS
);

В Effector обновление стора — декларативное с on():

// ── Effector ──────────────────────────────────────────────────
import { createStore, createEvent, combine } from 'effector';
const increment = createEvent();
const decrement = createEvent();
const reset = createEvent();
const addAmount = createEvent<number>();
const $count = createStore(0)
.on(increment, state => state + 1)
.on(decrement, state => state - 1)
.on(reset, () => 0)
.on(addAmount, (state, amount) => state + amount);
// Derived store (вычисляемый)
const $doubled = $count.map(n => n * 2);
const $isEven = $count.map(n => n % 2 === 0);
// Подписка
$count.watch(count => console.log('count:', count));
// ── RxJS (тот же результат) ────────────────────────────────────
import { Subject, merge, BehaviorSubject } from 'rxjs';
import { scan, map, distinctUntilChanged, share } from 'rxjs/operators';
const increment$ = new Subject<void>();
const decrement$ = new Subject<void>();
const reset$ = new Subject<void>();
const addAmount$ = new Subject<number>();
type CountAction =
| { type: 'INC' }
| { type: 'DEC' }
| { type: 'RESET' }
| { type: 'ADD'; amount: number };
const count$ = merge(
increment$.pipe(map(() => ({ type: 'INC' as const }))),
decrement$.pipe(map(() => ({ type: 'DEC' as const }))),
reset$ .pipe(map(() => ({ type: 'RESET' as const }))),
addAmount$.pipe(map(amount => ({ type: 'ADD' as const, amount }))),
).pipe(
scan((state, action) => {
switch (action.type) {
case 'INC': return state + 1;
case 'DEC': return state - 1;
case 'RESET': return 0;
case 'ADD': return state + action.amount;
}
}, 0),
share() // многоразовый поток
);
// Derived streams
const doubled$ = count$.pipe(map(n => n * 2), distinctUntilChanged());
const isEven$ = count$.pipe(map(n => n % 2 === 0), distinctUntilChanged());

Effector Effect автоматически создаёт .pending, .done, .fail события:

// ── Effector Effect ───────────────────────────────────────────
import { createEffect, createStore } from 'effector';
interface User { id: number; name: string; }
const fetchUserFx = createEffect(async (id: number): Promise<User> => {
const res = await fetch(\`/api/users/\${id}\`);
if (!res.ok) throw new Error('Not found');
return res.json();
});
const $user = createStore<User | null>(null).on(fetchUserFx.done, (_, { result }) => result);
const $loading = createStore(false)
.on(fetchUserFx.pending, (_, pending) => pending);
const $error = createStore<string | null>(null)
.on(fetchUserFx.fail, (_, { error }) => error.message)
.on(fetchUserFx, () => null); // сбрасываем при новом вызове
// Запуск:
fetchUserFx(42);
// ── RxJS (аналог Effect) ──────────────────────────────────────
import { Subject, BehaviorSubject } from 'rxjs';
import { switchMap, map, catchError, startWith, share } from 'rxjs/operators';
interface EffectState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
const fetchUser$ = new Subject<number>();
const userState$ = new BehaviorSubject<EffectState<User>>({
data: null, loading: false, error: null
});
fetchUser$.pipe(
switchMap(id =>
from(fetch(\`/api/users/\${id}\`).then(r => {
if (!r.ok) throw new Error('Not found');
return r.json() as Promise<User>;
})).pipe(
map(data => ({ data, loading: false, error: null })),
startWith({ data: null, loading: true, error: null }),
catchError(err => [{ data: null, loading: false, error: err.message }])
)
)
).subscribe(userState$);
// Запуск:
fetchUser$.next(42);

КритерийRxJSEffector
Гибкость⭐⭐⭐⭐⭐⭐⭐⭐
Предсказуемость⭐⭐⭐⭐⭐⭐⭐⭐
Кривая обученияВысокаяСредняя
Сложные потоки✅ Нативно⚠️ С трудом
Управление состоянием⚠️ Вручную✅ Нативно
DevToolsНетВстроенные
SSRНужна настройка✅ Из коробки
Размер бандла~48KB~10KB

Выбирай RxJS, если:

  • Сложная обработка потоков событий (WebSocket, real-time)
  • Нужны все операторы (debounce, throttle, zip, combineLatest)
  • Работаешь с Angular (RxJS там встроен)
  • Уже есть RxJS в стеке

Выбирай Effector, если:

  • Нужен чёткий, предсказуемый state manager
  • Команда предпочитает декларативный API
  • Важны DevTools и TypeScript из коробки
  • SSR и серверный рендеринг

Ошибка: пытаться симулировать Effector на RxJS без архитектуры

// ❌ Хаотичный код без структуры
const a$ = new Subject();
const b$ = new Subject();
// ... 50 subjects без имён и типов
// ✅ Чёткая доменная структура как в Effector
namespace UserDomain {
export const fetch = new Subject<number>();
export const logout = new Subject<void>();
export const state$ = new BehaviorSubject<UserState>(initial);
}


Попробуйте примеры в интерактивном редакторе: