65. Effector: Stores и Events
Effector: Stores и Events — строительные блоки
Заголовок раздела «Effector: Stores и Events — строительные блоки»Если Effector — это LEGO, то Store и Event — это самые главные кубики 🧩. Из них строится любая бизнес-логика: от простого счётчика до сложного мультишагового процесса. Давай разберём их досконально!
createStore() — создаём хранилище
Заголовок раздела «createStore() — создаём хранилище»import { createStore } from 'effector';
// Примитивные сторыconst $count = createStore(0);const $name = createStore('');const $flag = createStore(false);
// Стор с объектомconst $user = createStore<User | null>(null);
// Стор с массивомconst $todos = createStore<Todo[]>([]);
// С опциямиconst $config = createStore( { theme: 'dark', lang: 'ru' }, { name: '$config', // имя для DevTools skipVoid: false, // пропускать ли undefined });createEvent() — создаём событие
Заголовок раздела «createEvent() — создаём событие»Events — это не просто уведомления, это функции, которые несут данные (payload) и запускают изменения.
import { createEvent } from 'effector';
// Событие без данныхconst buttonClicked = createEvent();
// Событие с типизированным payloadconst nameUpdated = createEvent<string>();const todoAdded = createEvent<{ id: number; text: string; done: boolean }>();const filterChanged = createEvent<'all' | 'active' | 'done'>();const itemRemoved = createEvent<number>(); // передаём id
// Event — это просто функция! Вызываем её напрямую:nameUpdated('Яша');todoAdded({ id: 1, text: 'Учить Effector', done: false });buttonClicked();store.on() — подписка на события
Заголовок раздела «store.on() — подписка на события».on() — главный способ обновлять стор. Он принимает событие и чистую функцию (currentState, payload) => newState.
import { createStore, createEvent } from 'effector';
const incremented = createEvent<number>();const nameChanged = createEvent<string>();const userLoaded = createEvent<User>();const errorsCleared = createEvent();
const $count = createStore(0) .on(incremented, (state, delta) => state + delta);
const $name = createStore('') .on(nameChanged, (_, newName) => newName); // _ — игнорируем старое значение
const $user = createStore<User | null>(null) .on(userLoaded, (_, user) => user);
const $errors = createStore<string[]>([]) .on(errorsCleared, () => []); // возвращаем новый пустой массив
// Несколько событий на одном стореconst $form = createStore({ name: '', email: '', age: 0 }) .on(nameChanged, (state, name) => ({ ...state, name })) .on(emailChanged, (state, email) => ({ ...state, email })) .on(ageChanged, (state, age) => ({ ...state, age }));store.reset() — сброс к начальному значению
Заголовок раздела «store.reset() — сброс к начальному значению»import { createStore, createEvent } from 'effector';
const formSubmitted = createEvent();const formReset = createEvent();
const $form = createStore({ name: '', email: '' }) .on(formSubmitted, (state) => state) // обычно здесь сабмит через effect .reset(formReset); // сбрасывает к { name: '', email: '' }
// Можно сбрасывать несколько сторов одним событием:const $errors = createStore<string[]>([]).reset(formReset);const $status = createStore<'idle' | 'sending'>('idle').reset(formReset);combine() — объединяем сторы
Заголовок раздела «combine() — объединяем сторы»combine() создаёт новый стор, значение которого вычисляется из нескольких других. Аналог useMemo для сторов!
import { createStore, combine } from 'effector';
const $firstName = createStore('Яша');const $lastName = createStore('Иванов');const $age = createStore(25);
// Объединяем в объектconst $fullUser = combine({ firstName: $firstName, lastName: $lastName, age: $age,});// $fullUser.getState() => { firstName: 'Яша', lastName: 'Иванов', age: 25 }
// Объединяем с трансформациейconst $fullName = combine( $firstName, $lastName, (first, last) => \`\${first} \${last}\`);// $fullName.getState() => 'Яша Иванов'
// Derived store (производный): аналог useSelector с мемоизациейconst $isAdult = $age.map((age) => age >= 18);const $greeting = $fullName.map((name) => \`Привет, \${name}!\`);store.map() — трансформируем значение
Заголовок раздела «store.map() — трансформируем значение»const $todos = createStore<Todo[]>([]);
// Производные сторы — обновляются автоматически!const $doneTodos = $todos.map((todos) => todos.filter((t) => t.done));const $activeTodos = $todos.map((todos) => todos.filter((t) => !t.done));const $totalCount = $todos.map((todos) => todos.length);const $doneCount = $doneTodos.map((todos) => todos.length);const $hasCompleted = $doneCount.map((count) => count > 0);split() — разветвляем события
Заголовок раздела «split() — разветвляем события»split() позволяет разбить один поток событий на несколько по условию:
import { createEvent, split } from 'effector';
const messageReceived = createEvent<{ type: 'info' | 'error' | 'warn'; text: string }>();
// Разделяем поток по типу сообщенияconst { infoReceived, errorReceived, warnReceived } = split(messageReceived, { infoReceived: ({ type }) => type === 'info', errorReceived: ({ type }) => type === 'error', warnReceived: ({ type }) => type === 'warn',});
// Теперь подписываемся на каждый случай отдельно$infoLog.on(infoReceived, (log, msg) => [...log, msg.text]);$errorLog.on(errorReceived, (log, msg) => [...log, msg.text]);Реальный пример: менеджер задач
Заголовок раздела «Реальный пример: менеджер задач»import { createStore, createEvent, combine } from 'effector';
// --- Types ---interface Todo { id: number; text: string; done: boolean;}type Filter = 'all' | 'active' | 'done';
// --- Events ---const todoAdded = createEvent<string>(); // payload: текст задачиconst todoToggled = createEvent<number>(); // payload: id задачиconst todoRemoved = createEvent<number>(); // payload: id задачиconst filterChanged = createEvent<Filter>();const allCompleted = createEvent();const clearedDone = createEvent();
// --- Stores ---let nextId = 1;
const $todos = createStore<Todo[]>([]) .on(todoAdded, (todos, text) => [ ...todos, { id: nextId++, text, done: false } ]) .on(todoToggled, (todos, id) => todos.map((t) => t.id === id ? { ...t, done: !t.done } : t) ) .on(todoRemoved, (todos, id) => todos.filter((t) => t.id !== id) ) .on(allCompleted, (todos) => todos.map((t) => ({ ...t, done: true })) ) .on(clearedDone, (todos) => todos.filter((t) => !t.done) );
const $filter = createStore<Filter>('all') .on(filterChanged, (_, filter) => filter);
// --- Derived stores ---const $filteredTodos = combine($todos, $filter, (todos, filter) => { if (filter === 'active') return todos.filter((t) => !t.done); if (filter === 'done') return todos.filter((t) => t.done); return todos;});
const $stats = combine($todos, (todos) => ({ total: todos.length, done: todos.filter((t) => t.done).length, active: todos.filter((t) => !t.done).length,}));Читаем состояние вне компонентов
Заголовок раздела «Читаем состояние вне компонентов»// Получить текущее значение (не реактивно)const currentTodos = $todos.getState();const currentFilter = $filter.getState();
// Подписаться на изменения (вне React)const unsubscribe = $todos.watch((todos) => { console.log('Задачи обновились:', todos);});
// Отписатьсяunsubscribe();🔗 Полезные ссылки
Заголовок раздела «🔗 Полезные ссылки»Практика
Заголовок раздела «Практика»Попробуйте примеры в интерактивном редакторе: