19. RxJS vs Effector
RxJS vs Effector: Битва реактивностей ⚔️
Заголовок раздела «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>(); // Effectorconst buttonClicked$ = new Subject<void>(); // RxJS
// Store (Effector) ≈ BehaviorSubject (RxJS)// — хранит текущее значениеconst $counter = createStore(0); // Effectorconst 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);2. 🔄 Обновление стора: on() vs scan()
Заголовок раздела «2. 🔄 Обновление стора: on() vs scan()»В 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 streamsconst doubled$ = count$.pipe(map(n => n * 2), distinctUntilChanged());const isEven$ = count$.pipe(map(n => n % 2 === 0), distinctUntilChanged());3. 🌐 Асинхронные эффекты: Effect vs mergeMap
Заголовок раздела «3. 🌐 Асинхронные эффекты: Effect vs mergeMap»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);4. 🤔 Когда что выбирать?
Заголовок раздела «4. 🤔 Когда что выбирать?»| Критерий | RxJS | Effector |
|---|---|---|
| Гибкость | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| Предсказуемость | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Кривая обучения | Высокая | Средняя |
| Сложные потоки | ✅ Нативно | ⚠️ С трудом |
| Управление состоянием | ⚠️ Вручную | ✅ Нативно |
| 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 без имён и типов
// ✅ Чёткая доменная структура как в Effectornamespace UserDomain { export const fetch = new Subject<number>(); export const logout = new Subject<void>(); export const state$ = new BehaviorSubject<UserState>(initial);}🔗 Смотри также
Заголовок раздела «🔗 Смотри также»- 3. Subjects: горячие потоки — Subject и BehaviorSubject
- 7. mergeMap и switchMap — обработка async
- 19. RxJS как стейт-менеджер — BehaviorSubject store
- 21. RxJS + MobX — ещё одно сравнение
Практика
Заголовок раздела «Практика»Попробуйте примеры в интерактивном редакторе: