Subscribers run after the reducer. Middleware runs before it, wrapped around dispatch, which is where logging, crash reporting, analytics and every asynchronous pattern in Redux 3,824 live. A middleware is a function of three nested arrows, storeApi => next => action => result: storeApi gives you getState and dispatch, and next is the rest of the pipeline.
const counter = (s = 0, a) => a.type === 'counter/addedBy' ? s + a.payload : s;
const logger = store => next => action => {
console.log('->', action.type, 'before:', store.getState());
const result = next(action); // pass it on; omit this and the action is swallowed
console.log('<-', action.type, 'after: ', store.getState());
return result; // dispatch() returns whatever this returns
};
const cap = store => next => action => // rewrite the action rather than block it
action.type === 'counter/addedBy' && store.getState() >= 5
? next({ ...action, payload: 0 }) : next(action);
const store = configureStore({ reducer: counter, middleware: g => g().concat(logger, cap) });
for (const n of [7, 7]) store.dispatch({ type: 'counter/addedBy', payload: n });Output
-> counter/addedBy before: 0 <- counter/addedBy after: 7 -> counter/addedBy before: 7 <- counter/addedBy after: 7
The second add changed nothing: cap rewrote its payload before the reducer saw it. getState() reads the old state in the before log and the new one in the after log, because next returns only once the reducer has run.

Order is the array order: the first entry is outermost and sees the action first. Older code spells the same wiring createStore(reducer, applyMiddleware(a, b)).