skip to content

In Redux's applyMiddleware chain, in what order do middlewares see an action, and how does next(action) differ from storeAPI.dispatch(action)?

level: middleimportance: should knowfreq 42%

answer

  1. compose, right to left
  2. first argument is outermost
  3. next moves one step inward
  4. dispatch restarts from the top

basics

~20 s

applyMiddleware(a, b, c) makes a the outermost wrapper: actions flow a, b, c, then the reducer, and results flow back c, b, a. next(action) passes the action one step inward; storeAPI.dispatch(action) restarts it at a.

solid answer

~40 s

`applyMiddleware(a, b, c)` gives each middleware the store API, then builds the new dispatch with `compose(a, b, c)(store.dispatch)`, which is `a(b(c(store.dispatch)))`. So `a` is outermost: a dispatched action enters `a` first, `a`'s `next` is `b`, `b`'s `next` is `c`, and `c`'s `next` is the store's original `dispatch`, which runs the reducer. Code after `next(action)` runs in reverse, `c` then `b` then `a`, like nested function calls. `next(action)` continues **this** pass one step inward. `storeAPI.dispatch(action)` is the fully composed dispatch, so the action **starts over** at `a`. Use `next` to pass the current action on; use `storeAPI.dispatch` to emit a new action that should go through every middleware. Re-dispatching the same action unconditionally recurses until the stack overflows.

code

ts · 19 lines
ts
import { applyMiddleware, legacy_createStore as createStore, type Middleware } from 'redux'
import { rootReducer } from './rootReducer'

const tag = (name: string): Middleware => () => next => action => {
  console.log(`${name}: before`)
  const result = next(action)
  console.log(`${name}: after`)
  return result
}

const store = createStore(rootReducer, applyMiddleware(tag('a'), tag('b'), tag('c')))

store.dispatch({ type: 'app/pinged' })
// a: before
// b: before
// c: before   <- next here is the store's own dispatch (reducer runs)
// c: after
// b: after
// a: after

go deeper

for a junior

Remember that the first middleware passed to applyMiddleware runs first, and that next passes the action on while dispatch starts it over.

for a middle

Derive the order from compose (right to left, first is outermost), trace before and after code, and explain which one a thunk receives and why.

for a senior

Debug ordering problems: loggers printing functions, transforming middleware in the wrong place, recursion from re-dispatching, and dispatching during construction.

for a principal

Keep the middleware stack small and documented: every layer runs on every dispatch, and implicit ordering dependencies become a source of fragile coupling.

## How the chain is built `applyMiddleware` is a store enhancer. When the store is created it: 1. Creates the base store with the original `dispatch`. 2. Builds a `middlewareAPI` object: `getState`, plus a `dispatch` that forwards to *whatever the final composed dispatch turns out to be*. 3. Calls every middleware with that API, getting back a list of `next => action => …` functions. 4. Composes them: `dispatch = compose(...chain)(store.dispatch)`. 5. Returns the store with `dispatch` replaced by the composed one. `compose(f, g, h)` means `(...args) => f(g(h(...args)))`: **right to left**. So with `applyMiddleware(a, b, c)`: - `c` wraps the store's original `dispatch`. - `b` wraps `c`. - `a` wraps `b`, and `a`'s wrapper is what callers get as `store.dispatch`. ## The flow of one action 1. Caller runs `store.dispatch(action)` → enters **a**. 2. `a` does its "before" work, calls `next(action)` → enters **b**. 3. `b` calls `next(action)` → enters **c**. 4. `c` calls `next(action)` → the store's original `dispatch`: reducer runs, listeners are notified. 5. Control returns to **c**'s "after" code, then **b**'s, then **a**'s. 6. `a`'s return value is what the caller's `dispatch` returns. It is the same shape as nested function calls: in, down, back out, which is why a logger at `a` sees the state before and after everything inside it. ## `next` versus `storeAPI.dispatch` | | `next(action)` | `storeAPI.dispatch(action)` | |---|---|---| | Where the action goes | The next middleware inward | The start of the chain (`a`) | | Middleware it passes through | Only the ones after this one | All of them, including this one | | Typical use | Pass the current action along | Emit a new action, such as a thunk result | | Danger | Forgetting to call it drops the action | Re-dispatching the same action loops forever | The thunk middleware shows why both exist. It hands thunks `storeAPI.dispatch`, not `next`, so a thunk that dispatches another thunk still gets it handled, and a plain action dispatched from a thunk passes through **every** middleware, including loggers placed before the thunk middleware. ## Why order matters in practice - **Async middleware first.** Put the thunk middleware, or other middleware that consumes non-plain values, early. Middleware placed before it sees raw functions; middleware after it sees only plain objects. - **Loggers and reporters later.** A logger after the async middleware logs exactly the actions that reach reducers. - **Transforming middleware is order-sensitive.** If `a` adds `meta.timestamp` and `b` reads it, `a` must come first. - **Library-defined positions.** Some middleware documents where it expects to sit; Redux Toolkit's docs, for example, prepend its listener middleware ahead of the defaults. ## Reading a real chain Take `applyMiddleware(crashReporter, thunk, logger)`: - A **thunk** enters `crashReporter`, then reaches the thunk middleware, which calls the function and never passes it on, so `logger` never sees it. - Each **plain action the thunk dispatches** goes through `storeAPI.dispatch`, re-enters at `crashReporter`, passes through the thunk middleware untouched, reaches `logger` and then the reducer. - A **plain action from a component** takes the same path as the thunk's plain actions. So the logger prints exactly the actions reducers receive, and the crash reporter wraps everything. ## Return values flow outward What `store.dispatch(x)` returns is whatever the **outermost** middleware returns, which is normally whatever its `next` returned, and so on inward. The store's own `dispatch` returns the action; the thunk middleware returns the thunk's result instead. As long as every middleware returns `next`'s result, a component can `await dispatch(someThunk())` no matter how many layers sit outside the thunk middleware. One middleware that forgets to `return` breaks that for every caller. ## Two failure modes 1. **Infinite recursion.** `storeAPI => next => action => { storeAPI.dispatch(action); return next(action) }` re-enters itself on every dispatch until the stack overflows. Only dispatch a *different* action, or guard with a condition that eventually stops. 2. **Dispatch during construction.** Calling `storeAPI.dispatch` in the outer `storeAPI => …` function, while the chain is still being built, throws "Dispatching while constructing your middleware is not allowed", because the composed dispatch does not exist yet and other middleware would be skipped. ## Enhancers and middleware `applyMiddleware` is itself an enhancer, and its source comment notes that because middleware may be asynchronous, it should be the **first** enhancer in a composition. Middleware ordering, discussed above, is a separate question from enhancer ordering.

  • A plain action dispatched from inside a thunk: which middleware does it pass through?
    All of them, from the first. The thunk middleware passes thunks the composed dispatch from storeAPI, not its own next, so the new action starts at the outermost middleware, goes through the thunk middleware (which forwards non-functions to next) and every middleware after it, then reaches the reducer.
  • How can a middleware safely dispatch a follow-up action in response to one it sees?
    Call next(action) first so the original action is processed, then call storeAPI.dispatch with a different action, guarded by a condition that cannot match that follow-up action itself. Dispatching the same action type unconditionally re-enters the middleware and recurses without end.

It works like a stack of airport checkpoints. next walks you to the next desk further in; storeAPI.dispatch sends you back out to the entrance to queue through every desk again, which is right for a new passenger and an endless loop if you keep sending the same one.

saying these in an interview costs you the question

  • The last middleware passed to applyMiddleware sees the action first
  • next(action) skips the remaining middleware and calls the reducer
  • storeAPI.dispatch only reaches the middleware after the current one
  • Middleware order does not matter because every middleware sees every action
  • Redux detects a middleware that re-dispatches the same action and stops it