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

65. Effector: Stores и Events

Если Effector — это LEGO, то Store и Event — это самые главные кубики 🧩. Из них строится любая бизнес-логика: от простого счётчика до сложного мультишагового процесса. Давай разберём их досконально!

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
}
);

Events — это не просто уведомления, это функции, которые несут данные (payload) и запускают изменения.

import { createEvent } from 'effector';
// Событие без данных
const buttonClicked = createEvent();
// Событие с типизированным payload
const 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();

.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 }));
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() создаёт новый стор, значение которого вычисляется из нескольких других. Аналог 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}!\`);
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() позволяет разбить один поток событий на несколько по условию:

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();

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