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.
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.