Руководство по использованию
В этом разделе показано, как описать и запустить конечные автоматы с lite-fsm.
Основные концепции
Библиотека построена вокруг нескольких ключевых концепций.
Состояния и переходы
Автомат всегда находится в одном из явно описанных состояний. Переход в другое состояние происходит при обработке события (action).
Контекст
Контекст — это объект с данными автомата. Его можно обновлять при каждом переходе в reducer-е.
Эффекты
Эффекты — функции, которые выполняются при входе в указанное состояние. В них живут все побочные действия: запросы, таймеры, вызовы сервисов.
Менеджер автоматов
MachineManager объединяет несколько автоматов, маршрутизирует события и запускает эффекты.
Создание автомата
Автомат описывается через createMachine:
import { createMachine } from "@lite-fsm/core";
const toggle = createMachine({
config: {
INACTIVE: {
TOGGLE: "ACTIVE",
},
ACTIVE: {
TOGGLE: "INACTIVE",
},
},
initialState: "INACTIVE",
initialContext: {
lastToggledAt: null as number | null,
},
reducer: (state, action, { nextState }) => {
state.state = nextState;
if (action.type === "TOGGLE") {
state.context.lastToggledAt = action.payload.now;
}
},
effects: {
ACTIVE: () => console.log("Активировано"),
INACTIVE: () => console.log("Деактивировано"),
},
});Reducer-ы в примерах этого раздела написаны в стиле Immer — они мутируют draft и ничего не возвращают. Чтобы это работало, нужно подключить immerMiddleware (см. ниже). Без него reducer должен возвращать новый объект { state, context }.
Имена состояний и событий рекомендуем писать в
UPPER_SNAKE_CASE(IDLE,LOAD_DATA,RESOLVE). Это делает граф переходов читаемым и хорошо отличает event types от обычных значений.
Запуск через MachineManager
import { MachineManager } from "@lite-fsm/core";
import { immerMiddleware } from "@lite-fsm/middleware/immer";
const manager = MachineManager({ toggle }, { middleware: [immerMiddleware] });
manager.onTransition((prev, next, action) => {
console.log("Состояние изменилось:", { prev, next, action });
});
manager.transition({ type: "TOGGLE", payload: { now: Date.now() } });
const { state, context } = manager.getState().toggle;Передайте в manager несколько автоматов, если процессы должны работать рядом. Каждый action проходит через все зарегистрированные машины; обработают его только те, у которых есть подходящий переход:
const manager = MachineManager({ toggle, counter, profile });Внедрение зависимостей
Зависимости (сервисы, утилиты, доступ к manager-у) задаются через setDependencies. Они становятся доступны как поля параметра эффекта.
manager.setDependencies({
api: {
fetchData: () => fetch("/api/data").then((r) => r.json()),
},
getState: manager.getState,
});
const data = createMachine({
config: {
IDLE: {
FETCH: "LOADING",
},
LOADING: {
FETCH_RESOLVE: "READY",
FETCH_REJECT: "ERROR",
},
READY: {
RESET: "IDLE",
},
ERROR: {
RETRY: "LOADING",
RESET: "IDLE",
},
},
initialState: "IDLE",
initialContext: {
items: [] as string[],
error: null as string | null,
},
reducer: (state, action, { nextState }) => {
state.state = nextState;
if (action.type === "FETCH_RESOLVE") {
state.context.items = action.payload.items;
state.context.error = null;
}
if (action.type === "FETCH_REJECT") {
state.context.error = action.payload.error;
}
},
effects: {
LOADING: async ({ api, transition }) => {
try {
const items = await api.fetchData();
transition({ type: "FETCH_RESOLVE", payload: { items } });
} catch (error) {
transition({
type: "FETCH_REJECT",
payload: { error: error instanceof Error ? error.message : "unknown" },
});
}
},
},
});Внутри эффекта transition(...) отправляет новое событие через тот же manager.
Асинхронные эффекты
Эффект может быть async. Любой await внутри отрабатывает в обычном режиме:
effects: {
LOADING: async ({ api, transition }) => {
try {
const result = await api.fetchSomething();
transition({ type: "FETCH_RESOLVE", payload: { data: result } });
} catch (error) {
transition({
type: "FETCH_REJECT",
payload: { error: error instanceof Error ? error.message : "unknown" },
});
}
},
}Если нужно отменять промежуточные
transitionпри смене состояния, оборачивайте эффект вcreateEffect({ type: "latest", ... })или используйте собственныйcancelFn. Подробнее — в разделе Отмена эффектов.
Обработка событий в любом состоянии
Чтобы переход срабатывал из любого состояния, опишите его в специальном ключе "*". Явный переход в конкретном состоянии всегда приоритетнее wildcard:
const machine = createMachine({
config: {
STATE_A: {
EVENT_1: "STATE_B",
},
STATE_B: {
EVENT_2: "STATE_C",
},
STATE_C: {
EVENT_3: "STATE_A",
},
"*": {
RESET: "STATE_A",
LOG: null,
},
},
initialState: "STATE_A",
initialContext: {},
});Цель null — это self-transition: state не меняется, но action всё равно доходит до reducer-а. State-specific effects при этом не запускаются; для реакции на такие события используйте wildcard-effect.
Архитектура приложения на основе lite-fsm
Следующая sequence-диаграмма показывает базовый поток данных в приложении с lite-fsm:
Основные компоненты архитектуры:
-
Пользовательский интерфейс (UI): Отображает состояние приложения и отправляет события в
MachineManager. -
MachineManager: Центральный координатор, который:
- Хранит общее состояние всех автоматов
- Передает события в зарегистрированные автоматы
- Запускает эффекты при изменении состояний
- Уведомляет подписчиков об изменениях состояния
- Внедряет зависимости (сервисы, утилиты) в эффекты
-
Автоматы: Определяют:
- Состояния и переходы между ними
- Логику обновления контекста (данных)
- Эффекты, выполняемые при входе в определенные состояния
-
Сервисный слой: Содержит внешние зависимости:
- API-клиенты для взаимодействия с серверами
- Сервисы для работы с хранилищем, логированием и т.д.
- Утилиты и хелперы для обработки данных
Такая архитектура разделяет UI, модель состояния и внешние сервисы. Это упрощает тестирование и делает точки взаимодействия явными.
Преимущества выделения бизнес-логики в автоматы
Практичный способ использовать lite-fsm — держать поведение процесса в автоматах (config, reducer, effects) и сервисном слое. Компоненты пользовательского интерфейса при этом отображают данные и отправляют события.
1. Разделение ответственности
Традиционный подход с React Hooks:
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUser() {
setLoading(true);
try {
const response = await fetch("/api/user");
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchUser();
}, []);
// JSX с обработкой разных состояний...
}Подход с lite-fsm:
function UserProfile() {
const { state, context } = useSelector((s) => s.user);
const transition = useTransition();
useEffect(() => {
transition({ type: "FETCH_USER" });
}, [transition]);
// JSX с обработкой разных состояний...
}Бизнес-логика теперь живёт в автомате user: его состояние определяет UI, а эффекты выполняют все побочные действия.
2. Ключевые преимущества
Предсказуемость и отслеживаемость
- Явные состояния: основные состояния процесса описаны в
config - Единая точка входа: переходы проходят через
transition - Логирование и отладка: изменения состояния отслеживаются через middleware или
onTransition - Тестирование: reducer и effects проверяются отдельно от React-компонентов
- Аналитика: явные события проще отправлять во внешние системы
Управление сложностью
- Локализация изменений: изменения в процессе чаще остаются внутри одной машины
- Разделение UI и поведения: компонентам не нужно знать детали async-сценариев
- Контроль побочных эффектов: асинхронные операции сгруппированы в effects
- Декомпозиция: независимые процессы выносятся в отдельные автоматы
Границы кода
- Чистые компоненты: UI-компоненты фокусируются только на отображении и взаимодействии
- Переиспользование логики: бизнес-логика не привязана к компонентам
- Меньше привязки к UI: core-машины работают вне React
- Снижение дублирования: общая логика управления состояниями вынесена на уровень автоматов
Поведение UI
- Предсказуемые состояния UI: компонент читает state/context и показывает соответствующий экран
- Меньше неучтённых комбинаций: явные состояния помогают увидеть пропущенные сценарии
- Согласованное поведение: одинаковые события проходят через одну модель переходов
3. Сравнение с useEffect
Если сложный процесс реализован только через useEffect, часто появляются типичные проблемы:
- Разбросанность логики: Логика часто распределена между несколькими хуками в одном компоненте
- Нечёткие зависимости: Массив зависимостей часто становится источником ошибок и бесконечных циклов
- Сложность тестирования: Тестирование компонентов с несколькими
useEffectтребует сложной настройки - Отсутствие явной модели состояний: Состояния неявно определяются комбинациями переменных состояния
- Сложность отмены эффектов: функция очистки в
useEffectлегко расходится с бизнес-состоянием компонента
В подходе с lite-fsm эти риски уменьшаются за счёт другой структуры:
- Вся бизнес-логика централизована в автоматах
- Все состояния явно определены
- Переходы между состояниями контролируются автоматом
- Эффекты привязаны к конкретным состояниям
- Тестирование логики можно проводить без зависимости от React