Hummbitv1.x

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:

  • next is "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 after

For non-wrapper hooks:

  • afterUpdate(info) runs after a successful state update;
  • devtoolsFilter and devtoolsTransform are 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 async and await 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 ctx as 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 mergeState is 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.

Author: Alexey TolkachevLinkedIn