Effector — когда команда и фичи выходят на масштаб

Дополнительный модуль. Основной стек курса — TanStack Query (серверный стейт) + Zustand (клиентский). Здесь разбираем, когда имеет смысл поехать на Effector: команда растёт до 10+ человек, а фичи — стикеры, короткие видео, реакции, typing/presence — добавляют сложные асинхронные потоки.

Перед началом: Модули 1–8. Инструменты: Effector. Время: ~5 ч.


🔥 Инцидент. Команда выросла до 10 человек. Zustand-сторы — глобальные, любой может мутировать откуда угодно. Эффекты (запросы к API) живут в компонентах — тестировать невозможно. При SSR — гонки (hydrate mismatch). Effector даёт явные зависимости, изоляцию сторов и SSR-безопасность.


📖 Теория

Постановка вопроса

На MVP с ≤20 пользователями Zustand + Query — минимальный по TCO выбор. Но представим рост:

  • Команда 10+ фронтенд-разработчиков, параллельно пилят разные фичи.
  • Новые фичи: каталог стикеров, короткие видео (чанковый аплоуд + транскодинг), реакции, «печатает…», presence, статусы доставки/прочтения.
  • Требования: предсказуемость в чужом коде на ревью, единый паттерн оркестрации, лёгкое тестирование, трейсинг «что вызвало перерисовку/событие».

Вопрос: становится ли в этих условиях Effactor более правильным выбором, чем Zustand?

Почему Effector «раскрывается» на масштабе

КачествоНа ZustandНа EffectorПочему это важно для 10+ команды
Поток данныхнеявный (set(...))явный Event → Store → Effectна ревью видно, кто и зачем меняет стейт
Изоляция владения«плоские» сторы на фичуcreateDomain по фиче/командеразные люди не конфликтуют, чёткие границы (ложится на FSD)
Асинхрон-оркестрацияad-hoc async в экшенахsample, attach, guard, restoreединый паттерн для сложных потоков (видео, аплоуд, очереди)
Тестируемостьмокай стордиспетч events, assert stores (чистые функции)простые юнит-тесты, стабильный CI
Трейсинг/отладкаDevTools сторовграф событий + DevToolsнаходишь «откуда прилетело» в большой кодовой базе
«Скрытые» перерендерыредконет (явные подписки)предсказуемый перформанс на тяжёлых экранах

Итог: сильные стороны Zustand (простота, минимум кода) на масштабе превращаются в слабость — без единого паттерна каждая фича пишется «как получилось», а дебаг чужого потока становится дороже.

Как будущие фичи ложатся на Effector

Стикеры (каталог, поиск, recently-used)

Серверные данные (каталог, паки) — по-прежнему TanStack Query (кэш, инвалидация). Клиентское (выбор пака, недавно использованные, ввод) — Effector-стор + effects для подгрузки. sample связывает ввод → эффект → стор без расползания логики по компонентам.

Короткие видео (самая «тяжёлая» фича)

Аплоед — это state machine: выбор файла → чанковый аплоуд с прогрессом → поллинг статуса транскодинга → готово/ошибка → ретрай. На Effector это описывается декларативно:

ts
import { createEvent, createStore, createEffect, sample, guard } from "effector"

export const videoSelected = createEvent<File>()
export const retry = createEvent()

const uploadChunkFx = createEffect(async (chunk: Blob) => {
  /* POST chunk, вернуть прогресс */
})

export const $upload = createStore<{ progress: number; status: "idle" | "uploading" | "processing" | "done" | "error" }>({
  progress: 0,
  status: "idle",
})
  .on(uploadChunkFx.done, (s) => ({ ...s, progress: Math.min(100, s.progress + 10) }))
  .on(uploadChunkFx.fail, () => ({ progress: 0, status: "error" }))

// старт аплоуда при выборе файла
sample({ clock: videoSelected, target: uploadChunkFx })
// ретрай только из ошибки
guard({ clock: retry, source: $upload, filter: (s) => s.status === "error", target: uploadChunkFx })

На Zustand это та же логика, но размазанная по async-функциям и флагам — на масштабе её сложнее читать и тестировать. Effector даёт единый, трассируемый граф.

Реакции / typing / presence / read-receipts

Всё это — WebSocket-события → агрегация в сторе (debounce, дедуп, буфер). Effector хорошо ложится: WS-клиент кидает события, sample/guard фильтруют и обновляют сторы. Дедупликация по message_id — отдельный guard в одну строку.

Цена для команды 10+

Статья затратОценка
Онбординг одного разработчика на Effector8–16 ч (модель event/store/effect, sample/attach, домены)
Онбординг 10 человек~80–160 ч совокупно (можно сократить шаблонами и парными сессиями)
Наймсложнее, чем Zustand/Redux (пул меньше глобально, больше в RU/CIS)
Бойлерплейт на тривиальную фичувыше (Event + Store + Effect + sample); нужны шаблоны/кодогенерация, чтобы не терять скорость
Конвенции/линтингобязательно (effector/reducer, effector/no-ambiguity и т.п.) — иначе граф событий превратится в спагетти
Миграция с Zustandсредняя: клиентский стейт переносится по фичам, Query остаётся

Правило решения

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

  • команда ≥8–10 фронтендеров и/или быстрый рост;
  • в продукте сложные асинхронные потоки (медиа-аплоуд, очереди, многошаговые сценарии, тяжёлый real-time);
  • важна аудитируемость потока данных (финдфинг, compliance, частые ревью);
  • готов инвестировать в конвенции и онбординг.

Оставайся на Zustand + Query, если:

  • команда маленькая (1–4), MVP, ≤сотен пользователей;
  • фичи в основном CRUD + простой real-time;
  • приоритет — скорость и минимум абстракций.

Миграция Zustand → Effector (когда решили расти)

  1. Query остаётся — серверный кэш не трогаем.
  2. Переносим по одной фиче: стартуем с самой «потоковой» (например, отправка сообщения или виде-аплоуд) — на ней команда учится модели.
  3. Вводим domain-изоляцию по FSD-слайсам сразу — это половина пользы Effector на масштабе.
  4. Заводим ESLint-правила eslint-plugin-effector и шаблон фичи (Event/Store/Effect/sample) — чтобы не плодить вариации.
  5. Тесты: диспетч events → assert $store.getState() — это самое большое преимущество для CI.

Вердикт

  • Для курса и MVP — остаёмся на Zustand + TanStack Query (минимальный TCO, лучшая обучаемость).
  • Effector — документируем как «путь масштабирования»: понятный, предсказуемый, хорошо тестируемый выбор, когда команда вырастает до 10+ и появляются медиа-фичи и сложные потоки.
  • Стикеры/видео/реакции — это ровно те сценарии, ради которых Effector и стоит рассматривать: на них его граф событий и sample-оркестрация окупают более крутой порог входа.

Практика модуля: реализовать аплоуд короткого видео двумя способами (Zustand и Effector), сравнить читаемость, тестируемость и трейсинг. Это даст студентам честную картину компромисса.


💱 Компромиссы (Trade-offs)

РешениеПлюсыМинусы
Zustand + QueryМинимум кода, быстрый онбординг, низкий TCOГлобальные сторы, неявный поток, сложно на масштабе
Effector (Event→Store→Effect)Явный граф, изоляция доменов, лёгкие тестыБойлерплейт, выше порог входа
createDomain по фичамЧёткие границы владения, ложится на FSDНужно следить за структурой доменов
fork() для SSR/тестовИзоляция состояния без утечек между запросамиДисциплина: свой scope на запрос/тест
Миграция по одной фичиПостепенный переезд, Query не трогаемВременно две парадигмы в одной кодовой базе

🧩 Проверь себя

1. Effector serialize (serialize:Different) при SSR — зачем?
2. Effector лучше Redux/Zustand, когда...
3. fork() в Effector — для чего?
4. createDomain() в Effector — зачем?
5. Effector sample/guard — для чего?

🔗 Мостик к Модулю 10

Состояние масштабируемо. Измерим производительность — Core Web Vitals.