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

64. Effector: Введение

Effector: Введение в реактивное управление состоянием

Заголовок раздела «Effector: Введение в реактивное управление состоянием»

Effector — это мощная библиотека для управления состоянием, основанная на идеях реактивного программирования и event-driven архитектуры. Представь, что твой стейт — это электрическая сеть 🔌: события — это выключатели, сторы — аккумуляторы, а эффекты — электроприборы. Нажал выключатель → ток потёк → прибор заработал!

Effector создан в России командой под руководством Дмитрия Горячева и сегодня широко используется в крупных российских и зарубежных продуктах.

[Icon: Zap] Явный поток данных: Ты всегда знаешь, откуда пришли данные и куда они пойдут. [Icon: Package] Три кита: Store, Event, Effect — три примитива, из которых строится всё. [Icon: Cpu] Без магии: Никакого Proxy, никаких мутаций — чистые функциональные преобразования. [Icon: Globe] SSR из коробки: Fork API позволяет изолировать состояние на сервере. [Icon: Layers] TypeScript-first: Отличный вывод типов без лишних дженериков.

graph TD
Event[🎯 Event\nСобытие-триггер] --> Store
Store[📦 Store\nХранилище данных] --> UI[React компонент]
Store --> Effect
Effect[⚡ Effect\nАсинхронная операция] --> |done/fail| Store
UI --> |dispatch| Event

Store — это реактивная ячейка памяти. Он хранит текущее значение и уведомляет подписчиков при изменении.

import { createStore } from 'effector';
// Создаём стор с начальным значением
const $counter = createStore<number>(0);
const $userName = createStore<string>('');
const $isLoading = createStore<boolean>(false);
// По соглашению, имена сторов начинаются с $
// Это не обязательно, но очень помогает читать код!

Event — это функция-триггер. Она не хранит данных, только сигнализирует о том, что что-то произошло.

import { createEvent } from 'effector';
// Событие без данных
const buttonClicked = createEvent();
// Событие с данными (payload)
const userNameChanged = createEvent<string>();
const itemAdded = createEvent<{ id: number; title: string }>();
// Вызов события — это просто вызов функции!
buttonClicked(); // триггер без данных
userNameChanged('Яша'); // триггер с данными

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>

В 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 ToolkitMobXZustandEffector
ПарадигмаFlux / ReducerРеактивное ООПClosure StoreEvent-driven FP
BoilerplateСреднийМинимальныйМинимальныйМинимальный
TypeScriptХорошийХорошийОтличныйОтличный
SSRЧерез thunkСложноЧерез middlewareFork API (нативно)
Магия / ProxyНетДа (Proxy)НетНет
Независимость от ReactДаДаНетДа
Размер (gzip)~14kb~16kb~1kb~5kb
ТестируемостьХорошаяСредняяХорошаяОтличная
ОбучаемостьСредняяСредняяЛёгкаяСредняя
// ❌ Redux Toolkit
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
},
});
// ❌ Zustand
const useCounter = create((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
decrement: () => set((s) => ({ count: s.count - 1 })),
}));
// ✅ Effector
const incremented = createEvent();
const decremented = createEvent();
const $count = createStore(0)
.on(incremented, (s) => s + 1)
.on(decremented, (s) => s - 1);

Заметь, насколько читаемо решение на Effector: данные и логика явно разделены!

// model.ts — вся логика живёт здесь
import { createStore, createEvent, createEffect, sample } from 'effector';
// Events
export const pageOpened = createEvent();
export const searchChanged = createEvent<string>();
// Effects
export const loadUsersFx = createEffect(async () => {
const res = await fetch('/api/users');
return res.json();
});
// Stores
export 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-react
yarn add effector effector-react
# или
pnpm add effector effector-react

Для DevTools (расширение Redux DevTools):

Окно терминала
npm install effector-logger

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