What is the signature of a Redux middleware, and how would you write one that logs every action and the state it produces?
answer
- three nested functions
- storeAPI, then next, then action
- read before, call next, read after
- return what next returned
basics
~10 sA Redux middleware is storeAPI => next => action => result; a logger reads getState(), calls next(action) to let the action reach the reducer, reads getState() again, and returns next's result.
solid answer
~40 sMiddleware is three nested functions: `storeAPI => next => action => { ... }`. `storeAPI` holds only `getState` and `dispatch`; `next` is the next middleware in the chain, or the store's real `dispatch` for the last one; the innermost function runs once per dispatched action. A logger logs the action and `storeAPI.getState()`, calls `const result = next(action)` so the action carries on towards the reducer, then logs `getState()` again, which is now the new state because the reducer ran synchronously inside `next`, and finally returns `result` so callers of `dispatch` still get the return value. In Redux 5 the `action` parameter is typed `unknown`, so guard with `isAction(action)` before reading `action.type`. You install it with `applyMiddleware(logger)`.
code
ts · 16 linesimport { applyMiddleware, isAction, legacy_createStore as createStore, type Middleware } from 'redux'
import { rootReducer } from './rootReducer'
export const logger: Middleware = storeAPI => next => action => {
if (!isAction(action)) {
return next(action) // not a plain action: pass it on untouched
}
console.group(action.type)
console.log('prev state', storeAPI.getState())
const result = next(action) // reducer runs synchronously in here
console.log('next state', storeAPI.getState())
console.groupEnd()
return result
}
export const store = createStore(rootReducer, applyMiddleware(logger))go deeper
Write the three arrows from memory, storeAPI then next then action, and remember to call next(action) and return its result.
Explain when each layer runs, what storeAPI contains, why the state after next is already updated, and why action is typed unknown in Redux 5.
Reason about production middleware: placement relative to async middleware, cost on every dispatch, and error handling that never swallows actions silently.
Decide what deserves to be middleware at all, versus reducers, listeners or plain functions, keeping the dispatch path small and predictable for the whole team.
## Where middleware sits Redux's core `dispatch` does one thing: run the reducer and notify listeners. **Middleware** is the extension point that wraps `dispatch`, so code can run **between the moment an action is dispatched and the moment it reaches the reducer**. Logging, crash reporting, async logic and analytics all live here. `applyMiddleware(...middlewares)` is a store enhancer that builds the wrapped `dispatch` when the store is created. ## The signature, layer by layer ```ts const middleware = storeAPI => next => action => { /* ... */ } ``` Each arrow runs at a different time: 1. **`storeAPI => …`** runs **once, when the store is created**. `storeAPI` is a small object with exactly two members: `getState()` and `dispatch(action)`. It is not the store: there is no `subscribe` or `replaceReducer`. 2. **`next => …`** also runs **once**, when `applyMiddleware` composes the chain. `next` is the dispatch function of the next middleware, or the store's original `dispatch` if this middleware is last. 3. **`action => …`** runs **on every dispatch**. This is where your logic lives. Whatever it returns becomes the return value of the `dispatch` call that sent the action. The currying is not decoration: it lets each middleware capture the store API and its `next` once, in a closure, and do only the per-action work on each dispatch. ## Writing the logger The steps inside the per-action function: 1. **Guard the type.** In Redux 5, `action` is typed `unknown` because earlier middleware may pass functions or other values along. Use `isAction(action)` (exported by `redux`) before reading `action.type`; pass anything else straight to `next`. 2. **Read the state before**: `storeAPI.getState()`. 3. **Call `next(action)`** and keep the result. The action continues down the chain and, eventually, into the reducer, which runs **synchronously**. 4. **Read the state after.** Because the reducer has already run, `getState()` now returns the next state. 5. **Return the result**, so whoever called `dispatch` receives what the rest of the chain returned. ## Rules of thumb - **Always call `next` unless you mean to stop the action.** A middleware that forgets to call `next` silently swallows every action, and the reducer never sees it. - **Always return `next`'s result.** Middleware further along may return something useful, such as a promise; dropping it breaks `await dispatch(...)` for callers. - **Do not dispatch during setup.** Calling `storeAPI.dispatch` inside the outer `storeAPI => …` function throws "Dispatching while constructing your middleware is not allowed", because the chain does not exist yet. - **Keep it side-effect-aware.** Middleware is the right place for side effects, and the wrong place for state changes: those still go through reducers. ## Middleware compared with the other hooks Redux offers | Mechanism | Sees the action? | Runs before the reducer? | Can stop or replace the action? | |---|---|---|---| | Middleware | Yes | Yes, it wraps dispatch | Yes | | `store.subscribe` listener | No, no arguments | No, it runs after | No | | Reducer | Yes | It *is* the reducer | No, it only computes state | That table is why logging belongs in middleware: a subscriber cannot tell which action caused a change, and a reducer must stay free of side effects. ## Installing it With the core API, pass `applyMiddleware(logger)` as the enhancer argument: `legacy_createStore(rootReducer, applyMiddleware(logger))`. With Redux Toolkit, append it to the default middleware in `configureStore`. Where you place it in the chain decides what it sees. Put it **after** async middleware such as the thunk middleware and it logs only the plain actions that reach the reducer, not the functions that the thunk middleware intercepts. ## Error handling in middleware A crash-reporting middleware is the logger's sibling and shows one more rule: 1. Wrap `next(action)` in `try`/`catch`. 2. In `catch`, report the error together with the action and `storeAPI.getState()`, which is the context a bug report needs. 3. **Rethrow.** Swallowing the error would hide a broken reducer from the caller and from tests, and leave the app running on state nobody checked. Place it **outermost**, first in `applyMiddleware`, so it also catches errors thrown by every middleware inside it, not just by reducers. ## Why the logger shows the right "after" state Some candidates expect the reducer to run later, after the middleware returns. It does not: `next(action)` eventually calls the store's original `dispatch`, which runs the reducer and notifies listeners **before returning**. By the time control comes back to the logger, the new state is already in place.
- Why does storeAPI contain dispatch, and when would a middleware use it instead of next?storeAPI.dispatch is the fully wrapped dispatch, so an action sent through it passes through every middleware from the start of the chain. A middleware uses it to emit a new action that deserves the full treatment, for example a thunk dispatching a result. It uses next to pass the current action along.
- Can a middleware change the action before it reaches the reducer?Yes. It can call next with a different object, for example one with an added meta timestamp, or not call next at all to drop the action. That power is why middleware order matters and why transforming middleware should be kept rare and obvious.
saying these in an interview costs you the question
- A middleware receives the whole store, including subscribe
- The reducer runs after the middleware function returns
- Returning nothing from a middleware is fine because dispatch returns void
- A logger can be written as a store.subscribe listener that logs the action
- Middleware can safely dispatch while the chain is being built