Skip to the content.

Middleware is the store's pipeline, not a grab bag

Middleware is the store's pipeline, not a grab bag

Redux middleware has a reputation for being mysterious, mostly because of its famous triple-arrow signature. But the idea is plain: middleware is an ordered pipeline that every dispatched action flows through before it reaches the reducer. Each stage in the pipeline gets to inspect the action, and can pass it on, transform it, delay it, dispatch other actions, or swallow it entirely. Async handling, logging, crash reporting, analytics — they are all just stages in this one pipe. Once you see it as a pipeline with an order, the behaviour stops being magic and becomes predictable.

An action flows through logger, then thunk, then reaches the reducer dispatch sends an action into the middleware pipeline: logger, then thunk, then the reducer produces new state. A dot travels the pipeline left to right. dispatch logger thunk reducer state order matters: each stage wraps the next
Every action runs the pipeline in order before hitting the reducer; a stage can act, then call the next — or not.

The signature is a pipeline stage

The store => next => action => {} shape reads as three questions the pipeline asks each stage: here is the store, here is the next stage to hand off to, and here is the action. next(action) passes control down the pipe; whatever you do before and after that call is your stage’s behaviour. A logger is the clearest example:

const logger = (store) => (next) => (action) => {
  console.log("dispatching", action.type);
  const result = next(action);            // hand off to the next stage / reducer
  console.log("next state", store.getState());
  return result;
};

Before next, you see the action on the way in; after next, the reducer has run and you can see the new state. Every middleware is a variation on where it acts relative to that handoff.

A stage can transform or swallow

Because a stage decides whether and how to call next, it can do more than observe. The thunk middleware intercepts function actions and runs them instead of forwarding them to the reducer — a plain object it just passes along:

const thunk = (store) => (next) => (action) =>
  typeof action === "function"
    ? action(store.dispatch, store.getState)  // run it — never reaches the reducer
    : next(action);                            // ordinary action — pass down the pipe

That single conditional is the whole of “you can dispatch a function”: the middleware catches the function, so the reducer only ever sees plain actions.

Order is a real decision

Because each stage wraps the next, the order you compose them in changes behaviour. Put a logger before the thunk and you log the raw function action; put it after and you log the plain actions the thunk dispatches — very different logs. Put crash reporting last so it wraps everything, and analytics where it can see the resolved actions. This is not incidental; applyMiddleware(a, b, c) builds a(b(c(reducer))), and reversing the arguments reverses who sees what first. The apply-middleware and middleware-order exercises make you feel this directly — the same three stages in a different order produce a different trace, which is the fastest way to stop treating middleware as a grab bag and start treating it as the ordered pipe it is.