# Redux — Part 2: Middleware, Purity, and Why Redux Toolkit Exists

## 1. Why Plain Reducers Can't Do Async

Part 1's rule was absolute: reducers are pure, synchronous functions of `(state, action) => newState`. But real apps need to fetch data from a server, and an API call is neither pure (its result depends on network state, not just its arguments) nor synchronous (it returns a Promise). Redux's answer isn't "bend the reducer rule" — it's **middleware**: a layer that intercepts every dispatched action *before* it reaches any reducer, able to pause, inspect, delay, or dispatch further actions of its own.

```
dispatch(action) → [middleware 1] → [middleware 2] → ... → reducer → new state
```

## 2. What Middleware Actually Is

```ts
import { createStore, applyMiddleware } from "redux";

const loggerMiddleware = (store: any) => (next: any) => (action: any) => {
  console.log("dispatching:", action);
  const result = next(action); // pass the action along the chain
  console.log("next state:", store.getState());
  return result;
};

const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));
```

A middleware is a function with the signature `store => next => action => result` — a triple-nested arrow function. `next` is either the next middleware in the chain or, for the last one, the store's real dispatch that hands the action to the reducers. Calling `next(action)` continues the chain; NOT calling it stops the action from ever reaching the reducer at all — this is what lets a middleware swallow an action, delay it, or dispatch a completely different one instead.

## 3. Thunks: the Standard Async Middleware

```ts
// with redux-thunk middleware applied, dispatch can accept a FUNCTION,
// not just a plain action object
function fetchUser(userId: string) {
  return async (dispatch: any, getState: any) => {
    dispatch({ type: "user/loading" });
    try {
      const res = await fetch(`/api/users/${userId}`);
      const user = await res.json();
      dispatch({ type: "user/loaded", payload: user });
    } catch (err) {
      dispatch({ type: "user/error", payload: String(err) });
    }
  };
}

dispatch(fetchUser("42")); // thunk middleware intercepts the FUNCTION and calls it
```

Redux-thunk's middleware checks whether a dispatched "action" is actually a function; if so, it calls that function with `(dispatch, getState)` instead of passing it to reducers, letting the function dispatch as many real actions as it wants, whenever it wants (including after an `await`). This is how async logic — loading flags, success/error branching, sequencing multiple dispatches — lives outside the reducer entirely while still only ever *changing state* through ordinary, pure reducer-handled actions.

## 4. The Purity Requirement, Restated Precisely

```ts
// WRONG — impure: depends on Date.now() (external, non-deterministic input)
function reducer(state: Todo[], action: any) {
  if (action.type === "add") {
    return [...state, { id: Date.now(), text: action.payload }]; // breaks purity
  }
  return state;
}

// RIGHT — the id is generated OUTSIDE the reducer, passed in via the action
function addTodo(text: string) {
  return { type: "add", payload: { id: crypto.randomUUID(), text } };
}
```

A pure reducer must produce the exact same output every time it's called with the exact same `(state, action)` pair — no `Date.now()`, `Math.random()`, network calls, or reading global mutable variables inside the reducer body. Purity is what makes Redux's state predictable and replayable: given a sequence of actions, you can always recompute the exact same final state, which is the foundation both of Redux DevTools' time-travel debugging and of reliably testing a reducer as "call it, assert the output" with zero mocking.

## 5. Why Redux Toolkit Exists

Writing "plain" Redux at scale accumulates real boilerplate: a constants file for action type strings, an action-creator function for each action type, a switch-statement reducer per slice, manual immutable-update spreading for nested state, and manually wiring `applyMiddleware(thunk)` plus Redux DevTools. None of this is wrong, but it's a lot of ceremony for what's conceptually simple:

```ts
// plain Redux: type constant + action creator + switch-case reducer branch,
// three separate places to touch for ONE new capability
const TOGGLE_TODO = "todos/toggle";
function toggleTodo(id: string) { return { type: TOGGLE_TODO, payload: id }; }
// ...then a case TOGGLE_TODO: branch inside the reducer's switch...
```

Redux Toolkit (RTK), covered in the next topic, is the officially recommended way to write Redux today — `createSlice` generates the action creators and the reducer from one definition, `configureStore` wires up thunk middleware and DevTools automatically, and an Immer-powered "mutating" syntax lets reducer logic *read* like direct mutation while Immer produces the correct immutable update underneath. Nothing about Redux's underlying model (single store, dispatched actions, pure reducers) changes — RTK is a productivity layer on top of the exact same mental model this topic just covered.

## Under the Hood

**`applyMiddleware` builds a middleware chain by literally composing functions around the store's real `dispatch`.** Each middleware in the array you pass receives a reference to `store` (for `getState`/re-dispatching) and to `next`, which is either the next middleware's handler or, for the outermost call, the store's actual internal dispatch that runs the reducers and notifies subscribers. `applyMiddleware` wires this up via function composition (conceptually `middleware1(middleware2(middleware3(realDispatch)))`), which is why middleware order matters — logging middleware placed *before* thunk middleware in the array logs the raw function/thunk being dispatched (unhelpful), while placed *after* it only ever sees plain action objects that already passed through thunk's interception.

**Redux's `subscribe`/notification model is a flat list of listeners, fired synchronously after every dispatch completes.** There's no batching or scheduling built into plain Redux itself — calling `dispatch` runs the entire reducer tree synchronously, updates the internal state reference, and then calls every subscribed listener in registration order, all before `dispatch()` returns. `react-redux`'s `useSelector` is one such subscriber per connected component; it re-runs your selector function on every dispatch and compares the result via `Object.is` to the previous value, only triggering a React re-render when that comparison fails — which is the concrete mechanism behind Part 1's warning that reducers must return new references only when data actually changed.

![A dispatched action passes through the composed middleware chain such as thunk and logger before reaching the reducer; the new state notifies subscribers and useSelector re-renders on reference change](diagrams/01-middleware-data-flow.svg)

## Common Pitfalls

1. **Forgetting to apply thunk middleware** and then dispatching a function — without `applyMiddleware(thunk)` (or RTK's `configureStore`, which includes it by default), `dispatch(someFunction)` throws, because plain Redux's dispatch only accepts plain action objects.
2. **Putting side effects (API calls, `localStorage` writes) directly in a reducer** instead of a thunk/middleware — violates purity and makes the reducer's behavior depend on things outside its arguments.
3. **Middleware order mistakes** — placing logging middleware before thunk middleware in the `applyMiddleware` call logs unresolved thunk functions instead of the real actions they eventually dispatch.
4. **Assuming DevTools time-travel "just works" with impure reducers** — if a reducer reads `Date.now()`/`Math.random()`/external mutable state, replaying the same action sequence from a saved point produces a *different* result than what actually happened, breaking the entire replay guarantee.
5. **Writing large amounts of new plain-Redux boilerplate in a 2020s codebase** — the official Redux docs themselves recommend Redux Toolkit as the default starting point; hand-writing action-type constants and switch-statement reducers is now considered the "no longer necessary" pattern outside of maintaining older code or teaching the fundamentals (as this topic does).

## Quick Reference

```
middleware signature: store => next => action => result
applyMiddleware(m1, m2, ...)   → composes them around the store's real dispatch, IN ORDER

thunk: dispatch(fn) where fn = (dispatch, getState) => { ... }
       middleware detects a function was dispatched, calls it instead of passing to reducers

Purity rule: reducer(state, action) must be deterministic — no Date.now(), Math.random(),
             network calls, or external mutable reads inside the reducer body

Notification: dispatch runs reducers synchronously, THEN calls every subscriber
useSelector: re-runs selector after each dispatch, re-renders only if Object.is() differs

Why Redux Toolkit exists: createSlice + configureStore remove the boilerplate
(action-type constants, hand-written action creators, switch-statement reducers,
manual middleware/DevTools wiring) without changing the underlying model at all.
```
