@lite-fsm/middleware
@lite-fsm/middleware расширяет MachineManager: подключает Immer, отправляет события в Redux DevTools и даёт место для собственных middleware, например логирования, аналитики или сохранения состояния.
Установка
npm install @lite-fsm/middleware immerЧто такое middleware
Middleware в lite-fsm следует тому же паттерну, что и в Redux. Это функция формы:
const middleware = (api) => (next) => (action) => {
// код до next — видит состояние до перехода
const result = next(action);
// код после next — видит состояние после перехода
return result;
};MiddlewareApi | Что даёт |
|---|---|
getState() | Текущий state менеджера. |
transition(action) | Отправить action через всю middleware-цепочку. |
replaceReducer(enhancer) | Обернуть root reducer. |
onTransition(cb) | Подписка на каждый успешный dispatch. |
condition(predicate) | Promise, резолвится на matching action. |
Правила:
- порядок pre-
next= порядок регистрации, post-next— обратный (как в Redux); - код до
next(action)видит состояние до перехода, код после — состояние после перехода и уже отработавших подписчиков; - effects запускаются после возврата всей middleware-цепочки;
- чтобы заблокировать action — не вызывать
next(action); - чтобы изменить — вызвать
next(modifiedAction); transitionвозвращает action, дошедший до reducer-а.
Импорт
Импортируйте middleware из общего пакета или из точечных модулей:
// Общая точка входа — оба middleware
import { immerMiddleware, devToolsMiddleware } from "@lite-fsm/middleware";
// Точечные импорты помогают не включать лишний код
import { immerMiddleware } from "@lite-fsm/middleware/immer";
import { devToolsMiddleware } from "@lite-fsm/middleware/devTools";Встроенные middleware
immerMiddleware
Оборачивает root reducer в produce(...) из библиотеки Immer . Это позволяет мутировать state напрямую и не возвращать ничего из reducer-а.
import { MachineManager, createMachine } from "@lite-fsm/core";
import { immerMiddleware } from "@lite-fsm/middleware/immer";
const todos = createMachine({
config: {
READY: {
ADD_TODO: null,
TOGGLE_TODO: null,
REMOVE_TODO: null,
},
},
initialState: "READY",
initialContext: {
items: [] as { id: string; text: string; completed: boolean }[],
},
reducer: (state, action) => {
switch (action.type) {
case "ADD_TODO":
state.context.items.push({
id: action.payload.id,
text: action.payload.text,
completed: false,
});
return;
case "TOGGLE_TODO": {
const todo = state.context.items.find((t) => t.id === action.payload.id);
if (todo) todo.completed = !todo.completed;
return;
}
case "REMOVE_TODO":
state.context.items = state.context.items.filter((t) => t.id !== action.payload.id);
return;
}
},
});
const manager = MachineManager({ todos }, { middleware: [immerMiddleware] });immerMiddleware несёт маркер __liteFsmAllowVoidReducer, поэтому core разрешает reducer вернуть void. Если reducer всё-таки вернул объект, поля верхнего уровня копируются в draft, а неизменённые вложенные объекты сохраняют ссылки.
devToolsMiddleware
Подключает Redux DevTools Extension .
import { MachineManager } from "@lite-fsm/core";
import { devToolsMiddleware } from "@lite-fsm/middleware/devTools";
const manager = MachineManager(
{ counter },
{
middleware: [devToolsMiddleware({ blacklistActions: ["TIMER_TICK", "AUTO_SAVE"] })],
},
);| Опция | Назначение |
|---|---|
blacklistActions?: string[] | Action types, которые не отправляются в DevTools. |
Без window.__REDUX_DEVTOOLS_EXTENSION__ middleware пропускает события без отправки в DevTools. Если extension доступен: connect → init → send на каждый action; JUMP_TO_ACTION / ROLLBACK восстанавливают state через replaceReducer.
Важно: DevTools middleware находится в alpha-версии. Базовое логирование событий и time-travel restore работают; экспорт/импорт состояний и тестирование на основе записей пока не являются стабильным контрактом.
Создание собственного middleware
Middleware — функция формы (api) => (next) => (action) => action:
import type { Middleware } from "@lite-fsm/core";
const logger: Middleware = (api) => (next) => (action) => {
console.group(`Action: ${action.type}`);
console.log("before:", api.getState());
const result = next(action);
console.log("after:", api.getState());
console.groupEnd();
return result;
};
const manager = MachineManager({ counter }, { middleware: [logger] });Аналитика
const analytics: Middleware = (api) => (next) => (action) => {
const startedAt = performance.now();
const result = next(action);
const duration = performance.now() - startedAt;
window.analytics?.track("State Transition", {
action: action.type,
duration,
timestamp: new Date().toISOString(),
});
return result;
};Персистентность в localStorage
const persist =
(storageKey = "lite-fsm-state"): Middleware =>
(api) =>
(next) =>
(action) => {
const result = next(action);
try {
localStorage.setItem(storageKey, JSON.stringify(api.getState()));
} catch (err) {
console.error("Failed to persist state:", err);
}
return result;
};
const manager = MachineManager({ app }, { middleware: [persist("my-app-state")] });Для переноса состояния между сессиями используйте встроенные
manager.dehydrate()/manager.hydrate()— они корректно работают с акторами, версиями схемы иdehydrate-хуками. См. Сохранение состояния.
Централизованная обработка ошибок
const errorHandler =
(handler: (err: unknown, action: AnyEvent) => void): Middleware =>
(api) =>
(next) =>
(action) => {
try {
return next(action);
} catch (error) {
handler(error, action);
return action;
}
};
const manager = MachineManager(
{ app },
{
middleware: [
errorHandler((error, action) => {
reportErrorToService(error, { action });
showErrorNotification(`Произошла ошибка: ${(error as Error).message}`);
}),
],
},
);Порядок выполнения middleware
Middleware применяются в порядке регистрации до next, и в обратном — после next:
Action -> M1 -> M2 -> Reducer -> M2 -> M1 -> ResultПосле того как вся цепочка middleware вернётся, manager синхронно вызывает подписчиков onTransition, а уже после них — эффекты состояний.
Заключение
Middleware в lite-fsm дают единый механизм для интеграций: Immer, DevTools, логирование, аналитика, сохранение состояния и обработка ошибок. При этом они не меняют основную модель FSM: reducer и effects остаются отделены от интеграционного слоя.