Middleware
Middleware are read-only observers that watch the store without changing it. They are the right place for logging, analytics, devtools integration, and persistence.
import { type Middleware } from '@hurum/core'
const analytics = (): Middleware => ({ name: 'analytics', onEvent: (event, state) => { trackEvent(event.type, event) }, onError: (error, context) => { reportError(error, { intent: context.intent }) },})
// Register in the store builder chainconst MyStore = Store({ state: { count: 0 } }) .on({ /* ... */ }) .intents(MyIntents) .executors(MyExecutor) .middleware(logger(), devtools(), analytics())How it works
Section titled “How it works”A middleware is an object with a name and one or more optional hook methods. You register middleware via .middleware() at the end of the Store builder chain.
Middleware hooks are called after the action has already happened. They observe the result — they cannot prevent, modify, or intercept anything.
Middleware interface
Section titled “Middleware interface”interface Middleware { name: string onEvent?(event: StoreEvent, state: State): void onStateChange?(prevState: State, nextState: State): void onIntentStart?(intent: string, payload: unknown): void onIntentEnd?(intent: string, payload: unknown): void onError?(error: Error, context: ErrorContext): void}| Hook | Called when | Arguments |
|---|---|---|
onEvent | After an event is emitted and state is updated | The event object, the new state |
onStateChange | After state changes (including computed recalculation) | Previous state, next state |
onIntentStart | When an intent begins executing | Intent name, the payload |
onIntentEnd | When an intent finishes (success or failure) | Intent name, the payload |
onError | When an error occurs in an executor | The error, context with intent info |
Execution order
Section titled “Execution order”Middleware hooks are called at specific points in the intent lifecycle:
store.send(intent) → onIntentStart → executor runs → emit(event) → Store.on handler runs (state updates) → onEvent → onStateChange → (executor finishes) → onIntentEndIf an executor throws, onError is called instead of onIntentEnd.
Multiple middleware
Section titled “Multiple middleware”You can register multiple middleware. They are called in the order they are registered:
.middleware( logger(), // called first devtools(), // called second analytics(), // called third)Common patterns
Section titled “Common patterns”Logging middleware
Section titled “Logging middleware”const logger = (): Middleware => ({ name: 'logger', onEvent: (event, state) => { console.log(`[Event] ${event.type}`, event) }, onStateChange: (prev, next) => { console.log('[State]', { prev, next }) }, onIntentStart: (intent, payload) => { console.log(`[Intent Start] ${intent}`, payload) }, onIntentEnd: (intent, payload) => { console.log(`[Intent End] ${intent}`, payload) }, onError: (error, context) => { console.error(`[Error] in ${context.intent}:`, error) },})Analytics middleware
Section titled “Analytics middleware”const analyticsMiddleware = (tracker: AnalyticsTracker): Middleware => ({ name: 'analytics', onEvent: (event) => { // Track specific events if (event.type.startsWith('Checkout/')) { tracker.track(event.type, event) } }, onError: (error, context) => { tracker.trackError(error, { intent: context.intent, timestamp: Date.now(), }) },})Persistence middleware
Section titled “Persistence middleware”const persist = (storageKey: string): Middleware => ({ name: 'persist', onStateChange: (_prev, next) => { localStorage.setItem(storageKey, JSON.stringify(next)) },})Conditional middleware
Section titled “Conditional middleware”Since middleware are just objects, you can conditionally include them:
const MyStore = Store({ state: { /* ... */ } }) .on({ /* ... */ }) .middleware( ...[ process.env.NODE_ENV === 'development' && logger(), process.env.NODE_ENV === 'development' && devtools(), analytics(), ].filter(Boolean) as Middleware[], )Built-in middleware
Section titled “Built-in middleware”Hurum ships with several built-in middleware:
| Middleware | Purpose |
|---|---|
logger() | Logs events, state changes, intents, and errors to the console |
devtools() | Integrates with browser devtools for time-travel debugging |
persist() | Persists state to localStorage or a custom storage backend |
undoRedo() | Tracks state history for undo/redo support |
- Middleware cannot modify state. If you need to react to an event and trigger new behavior, use relay or emit additional events from an executor. Middleware is strictly observe-only.
- Keep middleware lightweight. Middleware hooks run synchronously in the state update path. Heavy computation (like serializing large state for persistence) should be deferred with
requestIdleCallbackor similar. - Use
namefor debugging. Thenameproperty helps identify which middleware is which in error messages and devtools. - Don’t put business logic in middleware. Logging what happened is fine. Deciding what should happen next belongs in executors and relay handlers.