64. Effector: Введение
Effector: Введение в реактивное управление состоянием
Заголовок раздела «Effector: Введение в реактивное управление состоянием»Effector — это мощная библиотека для управления состоянием, основанная на идеях реактивного программирования и event-driven архитектуры. Представь, что твой стейт — это электрическая сеть 🔌: события — это выключатели, сторы — аккумуляторы, а эффекты — электроприборы. Нажал выключатель → ток потёк → прибор заработал!
Effector создан в России командой под руководством Дмитрия Горячева и сегодня широко используется в крупных российских и зарубежных продуктах.
Почему Effector?
Заголовок раздела «Почему Effector?»[Icon: Zap] Явный поток данных: Ты всегда знаешь, откуда пришли данные и куда они пойдут. [Icon: Package] Три кита: Store, Event, Effect — три примитива, из которых строится всё. [Icon: Cpu] Без магии: Никакого Proxy, никаких мутаций — чистые функциональные преобразования. [Icon: Globe] SSR из коробки: Fork API позволяет изолировать состояние на сервере. [Icon: Layers] TypeScript-first: Отличный вывод типов без лишних дженериков.
Три примитива (Units)
Заголовок раздела «Три примитива (Units)»graph TD Event[🎯 Event\nСобытие-триггер] --> Store Store[📦 Store\nХранилище данных] --> UI[React компонент] Store --> Effect Effect[⚡ Effect\nАсинхронная операция] --> |done/fail| Store UI --> |dispatch| Event📦 Store — хранилище данных
Заголовок раздела «📦 Store — хранилище данных»Store — это реактивная ячейка памяти. Он хранит текущее значение и уведомляет подписчиков при изменении.
import { createStore } from 'effector';
// Создаём стор с начальным значениемconst $counter = createStore<number>(0);const $userName = createStore<string>('');const $isLoading = createStore<boolean>(false);
// По соглашению, имена сторов начинаются с $// Это не обязательно, но очень помогает читать код!🎯 Event — событие
Заголовок раздела «🎯 Event — событие»Event — это функция-триггер. Она не хранит данных, только сигнализирует о том, что что-то произошло.
import { createEvent } from 'effector';
// Событие без данныхconst buttonClicked = createEvent();
// Событие с данными (payload)const userNameChanged = createEvent<string>();const itemAdded = createEvent<{ id: number; title: string }>();
// Вызов события — это просто вызов функции!buttonClicked(); // триггер без данныхuserNameChanged('Яша'); // триггер с данными⚡ Effect — эффект (асинхронные операции)
Заголовок раздела «⚡ Effect — эффект (асинхронные операции)»Effect — это обёртка над async-функцией с автоматическим управлением состоянием загрузки.
import { createEffect } from 'effector';
// Создаём эффект для загрузки данныхconst fetchUserFx = createEffect<number, User, Error>(async (userId) => { const res = await fetch(`/api/users/${userId}`); if (!res.ok) throw new Error('Не удалось загрузить пользователя'); return res.json();});
// У каждого эффекта автоматически появляются:// fetchUserFx.pending — $store<boolean>// fetchUserFx.done — Event<{params, result}>// fetchUserFx.fail — Event<{params, error}>// fetchUserFx.doneData — Event<result>// fetchUserFx.failData — Event<error>Философия Effector: явный поток данных
Заголовок раздела «Философия Effector: явный поток данных»В Redux ты диспатчишь экшены → редьюсер меняет стейт (неявно через switch/case). В MobX ты мутируешь объект → магия Proxy обновляет всё (неявно через @observable). В Effector ты явно связываешь: «это событие → меняет этот стор → вот так».
import { createStore, createEvent } from 'effector';
const incremented = createEvent();const decremented = createEvent();const reset = createEvent();
const $counter = createStore(0) .on(incremented, (state) => state + 1) // явная связь .on(decremented, (state) => state - 1) // явная связь .reset(reset); // сброс к начальному значению
// Это читается как: "Когда произошло incremented, стор добавляет 1"// Никакой магии — просто функции!Сравнение с другими библиотеками
Заголовок раздела «Сравнение с другими библиотеками»| Характеристика | Redux Toolkit | MobX | Zustand | Effector |
|---|---|---|---|---|
| Парадигма | Flux / Reducer | Реактивное ООП | Closure Store | Event-driven FP |
| Boilerplate | Средний | Минимальный | Минимальный | Минимальный |
| TypeScript | Хороший | Хороший | Отличный | Отличный |
| SSR | Через thunk | Сложно | Через middleware | Fork API (нативно) |
| Магия / Proxy | Нет | Да (Proxy) | Нет | Нет |
| Независимость от React | Да | Да | Нет | Да |
| Размер (gzip) | ~14kb | ~16kb | ~1kb | ~5kb |
| Тестируемость | Хорошая | Средняя | Хорошая | Отличная |
| Обучаемость | Средняя | Средняя | Лёгкая | Средняя |
Пример: то же самое на всех библиотеках
Заголовок раздела «Пример: то же самое на всех библиотеках»// ❌ Redux Toolkitconst counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1; }, decrement: (state) => { state.value -= 1; }, },});
// ❌ Zustandconst useCounter = create((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })), decrement: () => set((s) => ({ count: s.count - 1 })),}));
// ✅ Effectorconst incremented = createEvent();const decremented = createEvent();const $count = createStore(0) .on(incremented, (s) => s + 1) .on(decremented, (s) => s - 1);Заметь, насколько читаемо решение на Effector: данные и логика явно разделены!
Архитектура приложения с Effector
Заголовок раздела «Архитектура приложения с Effector»// model.ts — вся логика живёт здесьimport { createStore, createEvent, createEffect, sample } from 'effector';
// Eventsexport const pageOpened = createEvent();export const searchChanged = createEvent<string>();
// Effectsexport const loadUsersFx = createEffect(async () => { const res = await fetch('/api/users'); return res.json();});
// Storesexport const $users = createStore<User[]>([]);export const $search = createStore('');export const $isLoading = loadUsersFx.pending;
// Logic (связи между юнитами)$search.on(searchChanged, (_, value) => value);$users.on(loadUsersFx.doneData, (_, users) => users);
// sample: загружаем пользователей при открытии страницыsample({ clock: pageOpened, target: loadUsersFx });// ui.tsx — компонент просто отображает данныеimport { useStore, useEvent } from 'effector-react';import { $users, $search, $isLoading, searchChanged, pageOpened } from './model';
function UserList() { const users = useStore($users); const search = useStore($search); const isLoading = useStore($isLoading); const handleSearch = useEvent(searchChanged);
// UI-логики минимум — только отображение и вызов событий return ( <div> <input value={search} onChange={e => handleSearch(e.target.value)} /> {isLoading ? <Spinner /> : users.map(u => <UserCard key={u.id} user={u} />)} </div> );}Установка
Заголовок раздела «Установка»npm install effector effector-reactyarn add effector effector-react# илиpnpm add effector effector-reactДля DevTools (расширение Redux DevTools):
npm install effector-logger🔗 Полезные ссылки
Заголовок раздела «🔗 Полезные ссылки»- Effector: Stores и Events
- Effector: Effects
- Effector + React
- Effector: Продвинутый уровень
- Zustand: Основы
- Обзор State Management
Практика
Заголовок раздела «Практика»Попробуйте примеры в интерактивном редакторе: