Middleware Deep Dive
Middleware in hummbit is a composable interception layer around state transitions and DevTools events.
Use it to add cross-cutting behavior without polluting domain actions:
- logging and tracing;
- analytics and telemetry;
- performance timing;
- invariants and runtime guards;
- event normalization before sending to DevTools.
1) Mental model
Each middleware receives a context object (ctx) and returns any subset of hooks.
You can implement one hook or several hooks in the same middleware.
Supported hooks:
wrapAction(name, next)wrapSetState(next)wrapMergeState(next)afterUpdate(info)devtoolsFilter(event)devtoolsTransform(event)
Think of wrappers (wrap*) as function decorators:
nextis "the current implementation";- your hook can run code before and/or after
next; - your hook decides whether to pass arguments as-is, modify them, or short-circuit.
2) Execution pipeline and order
Middleware are applied in array order. For wrappers this means the first middleware becomes the outermost layer:
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] })
// call order:
// m1 before -> m2 before -> action body -> m2 after -> m1 afterFor non-wrapper hooks:
afterUpdate(info)runs after a successful state update;devtoolsFilteranddevtoolsTransformare evaluated for each outgoing DevTools event in middleware order.
3) Hook-by-hook details
wrapAction(name, next)
Intercepts actions created via actionCreator(...).
Typical usage:
- timing action latency;
- attaching trace ids;
- global try/catch with error reporting;
- measuring args/result for debugging.
Notes:
- actions can be sync or async, so wrappers should usually be
asyncandawait next(...); - rethrow errors unless you intentionally convert them.
wrapSetState(next)
Intercepts setState calls.
Useful for:
- root-level validation before applying next state;
- deduplicating redundant writes;
- collecting "write reason" metrics.
This hook sees all direct setState updates, including those made inside actions.
wrapMergeState(next)
Intercepts mergeState calls (root-level shallow merge API).
Typical usage:
- enforcing allowed keys for partial updates;
- normalizing partial payloads;
- auditing merge-based updates separately from full updates.
afterUpdate(info)
Post-update notification hook. Use for:
- non-blocking side effects that should not mutate store logic;
- emitting telemetry after state is committed;
- syncing to external observers.
Keep this hook lightweight to avoid slowing down update throughput.
devtoolsFilter(event)
Returns whether a DevTools event should be forwarded. Use it to reduce noise (for example, hide repetitive low-level events).
devtoolsTransform(event)
Transforms a DevTools event payload before forwarding. Use it to:
- redact sensitive fields;
- shorten large payloads;
- normalize action/event naming.
Recommended pattern: filter first, transform second (in the same middleware or in separate middleware in intended order).
4) Context (ctx) guidance
The middleware context gives access to store-adjacent runtime capabilities (such as reading current state through provided APIs and configuration references available to middleware). General guidance:
- treat
ctxas read-mostly infrastructure context; - avoid storing mutable shared objects in module scope;
- prefer deterministic behavior (same input -> same output), except for explicitly side-effecting hooks.
5) Practical patterns
Logging 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);
},
});Runtime invariant middleware
const invariantMw: Middleware<RootState> = () => ({
wrapSetState: (next) => async (updater) => {
await next(updater);
// Example: assert that counter never drops below zero
// (replace with your domain checks)
},
});DevTools redaction middleware
const redactSecrets: Middleware<RootState> = () => ({
devtoolsTransform: (event) => ({
...event,
state: {
...event.state,
auth: event.state?.auth
? { ...event.state.auth, token: "***redacted***" }
: event.state?.auth,
},
}),
});6) Common pitfalls
- Forgetting
return await next(...)in wrappers, which can break action result flow. - Swallowing errors unintentionally in global try/catch wrappers.
- Heavy synchronous work in
afterUpdate, causing UI stalls. - Mutating input objects directly in transforms instead of returning new values.
- Assuming
mergeStateis deep merge: it is shallow at root level.
7) Performance recommendations
- Keep wrappers thin; move expensive logic to sampled/conditional paths.
- Avoid serializing large state snapshots on every hook call.
- Prefer feature-flagged middleware in development if diagnostics are expensive.
- Benchmark with production-like load before enabling heavy middleware globally.
8) Local store vs global singleton setup
Middleware can be configured:
- per store instance via
initStore(..., { ... })where applicable in your setup; - globally via
configureGlobalStore({ middleware: [...] })for singleton mode (hummbit/react).
In singleton mode, call configureGlobalStore(...) at app bootstrap before stores/actions are used.

