Middleware: подробности
Middleware в hummbit — это композиционный слой перехвата вокруг переходов состояния и событий DevTools.
Он нужен для сквозной (cross-cutting) логики без засорения доменных action:
- логирование и трассировка;
- аналитика и телеметрия;
- замер производительности;
- инварианты и runtime-проверки;
- нормализация событий перед отправкой в DevTools.
1) Ментальная модель
Каждый middleware получает контекст (ctx) и возвращает любой набор hook-ов.
Можно реализовать один hook или несколько hook-ов в одном middleware.
Поддерживаемые hooks:
wrapAction(name, next)wrapSetState(next)wrapMergeState(next)afterUpdate(info)devtoolsFilter(event)devtoolsTransform(event)
wrap*-hooks работают как декораторы функций:
next— это "текущая реализация";- ваш hook может выполнить код до и/или после
next; - можно пробрасывать аргументы как есть, модифицировать их или делать short-circuit.
2) Пайплайн выполнения и порядок
Middleware применяются по порядку в массиве. Для wrapper-хуков это означает: первый middleware становится самым внешним слоем:
const m1: Middleware<S> = () => ({
wrapAction:
(name, next) =>
async (...args) => {
console.log("m1 before", name);
const result = await next(...args);
console.log("m1 after", name);
return result;
},
});
const m2: Middleware<S> = () => ({
wrapAction:
(name, next) =>
async (...args) => {
console.log("m2 before", name);
const result = await next(...args);
console.log("m2 after", name);
return result;
},
});
// configureGlobalStore({ middleware: [m1, m2] })
// порядок вызовов:
// m1 before -> m2 before -> action body -> m2 after -> m1 afterДля не-wrapper hook-ов:
afterUpdate(info)вызывается после успешного обновления состояния;devtoolsFilterиdevtoolsTransformприменяются к каждому исходящему событию DevTools в порядке middleware.
3) Подробно по каждому hook
wrapAction(name, next)
Перехватывает action, созданные через actionCreator(...).
Типичные сценарии:
- замер latency action;
- добавление trace id;
- глобальный try/catch с отправкой ошибок;
- сбор аргументов/результата для отладки.
Нюансы:
- action могут быть sync и async, поэтому wrapper обычно делают
asyncиawait next(...); - ошибки лучше пробрасывать дальше (
throw), если вы не делаете осознанное преобразование.
wrapSetState(next)
Перехватывает вызовы setState.
Полезно для:
- root-валидации перед применением нового состояния;
- дедупликации повторных записей;
- метрик "почему произошла запись".
Этот hook видит все прямые setState, включая вызовы внутри action.
wrapMergeState(next)
Перехватывает вызовы mergeState (API shallow merge по корню).
Применяется для:
- контроля разрешённых ключей в partial update;
- нормализации частичных payload;
- отдельного аудита merge-обновлений.
afterUpdate(info)
Hook пост-обновления. Подходит для:
- неблокирующих side effects, которые не должны мутировать store-логику;
- отправки телеметрии после фиксации состояния;
- синхронизации с внешними наблюдателями.
Держите этот hook лёгким, чтобы не ухудшать пропускную способность обновлений.
devtoolsFilter(event)
Возвращает, нужно ли отправлять событие в DevTools. Используется для снижения шума (например, скрытия повторяющихся низкоуровневых событий).
devtoolsTransform(event)
Преобразует payload события перед отправкой в DevTools. Полезно для:
- редактирования чувствительных полей;
- сокращения слишком больших payload;
- нормализации именования action/event.
Рекомендуемый паттерн: сначала фильтрация, затем трансформация (в одном middleware или разнесённо по нескольким в нужном порядке).
4) Рекомендации по ctx
Контекст middleware даёт доступ к runtime-возможностям рядом со store (например, чтение текущего состояния через предоставленные API и конфигурационные ссылки, доступные middleware). Практические правила:
- используйте
ctxкак преимущественно read-only инфраструктурный контекст; - не храните изменяемые shared-объекты в module scope;
- стремитесь к детерминированному поведению (один и тот же вход -> один и тот же выход), кроме явно side-effect hook-ов.
5) Практические паттерны
Логирующий middleware
const logger: Middleware<RootState> = () => ({
wrapAction:
(name, next) =>
async (...args) => {
const t0 = performance.now();
try {
return await next(...args);
} finally {
console.log(`[action] ${name} took ${performance.now() - t0}ms`);
}
},
afterUpdate: ({ type }) => {
console.log("[update]", type);
},
});Middleware с runtime-инвариантами
const invariantMw: Middleware<RootState> = () => ({
wrapSetState: (next) => async (updater) => {
await next(updater);
// Пример: проверить, что counter не уходит ниже нуля
// (замените на ваши доменные проверки)
},
});Middleware для редактирования чувствительных данных в DevTools
const redactSecrets: Middleware<RootState> = () => ({
devtoolsTransform: (event) => ({
...event,
state: {
...event.state,
auth: event.state?.auth
? { ...event.state.auth, token: "***redacted***" }
: event.state?.auth,
},
}),
});6) Частые ошибки
- Забывают
return await next(...)в wrapper — ломается цепочка возврата результата. - Случайно "глотают" ошибки в глобальном try/catch.
- Делают тяжёлую синхронную работу в
afterUpdate, что может приводить к фризам UI. - Мутируют входные объекты в transform вместо возврата новых значений.
- Ожидают deep merge от
mergeState, хотя это shallow merge по корню.
7) Рекомендации по производительности
- Делайте wrappers тонкими; тяжёлую диагностику выносите в conditional/sampled логику.
- Не сериализуйте большие snapshot state на каждый hook без необходимости.
- Для дорогих диагностических middleware используйте feature flags, особенно в dev-only.
- Перед глобальным включением тяжёлого middleware проверяйте нагрузку на production-подобном профиле.
8) Настройка для local store и global singleton
Middleware можно конфигурировать:
- для конкретного store instance через
initStore(..., { ... })(где применимо в вашем setup); - глобально через
configureGlobalStore({ middleware: [...] })для singleton mode (hummbit/react).
В singleton mode вызывайте configureGlobalStore(...) на bootstrap до использования store/action.

