Skip to the code
cf.

Redux applyMiddleware

Redux middleware sits between dispatch and the reducer. It can log actions, change them, stop them, or dispatch other actions, which is how Redux handles async work. Write applyMiddleware(...middlewares), the function Redux uses to add middleware to a store.

A middleware is a function in three layers: ({ getState, dispatch }) => (next) => (action) => result. The first layer gets the store's API. The second gets next, a function that passes an action on to the next middleware. The third handles each action, and what it returns is what dispatch returns.

applyMiddleware(...middlewares) returns an enhancer: a function that takes createStore and returns a new createStore. Calling that with (reducer, initialState) creates a store with the given createStore, then returns a copy of that store whose dispatch goes through the middleware. Everything else on the store, like getState and subscribe, stays the same.

createStore is any function like the one from the Redux-style store problem: createStore(reducer, initialState) returns an object with getState and dispatch, and maybe more methods.

An action dispatched to the new store goes through the middleware in the order they're listed. The next of the last middleware is the original store's dispatch, which runs the reducer. A middleware that doesn't call next stops the action there.

The dispatch that middleware get in their first layer is the new store's dispatch, so an action a middleware dispatches goes through all the middleware again, from the first. getState returns the store's current state.

The first two layers of each middleware run once, when the store is created; only the third runs for each action. A middleware that calls dispatch in its first layer, while the store is still being set up, gets an Error with the message "Dispatching while constructing your middleware is not allowed".

const logger = ({ getState }) => (next) => (action) => {
  console.log("before", getState());
  const result = next(action);
  console.log("after", getState());
  return result;
};
const thunk = ({ dispatch, getState }) => (next) => (action) =>
  typeof action === "function" ? action(dispatch, getState) : next(action);
 
const counter = (state = 0, action) => (action.type === "inc" ? state + 1 : state);
const store = applyMiddleware(thunk, logger)(createStore)(counter, 0);
 
store.dispatch({ type: "inc" }); // logs "before 0", then "after 1"
store.dispatch((dispatch) => {
  dispatch({ type: "inc" });     // logs "before 1", then "after 2"
  return "done";
});                              // returns "done"
store.getState();                // 2

What the tests check

  1. with no middleware, dispatch works as before
    const createStore = (reducer, state) => ({
      getState: () => state,
      dispatch(action) {
        state = reducer(state, action);
        return action;
      },
    });
    const counter = (state, action) => (action.type === "inc" ? state + 1 : state);
    const store = applyMiddleware()(createStore)(counter, 5);
    const action = { type: "inc" };
    expect(store.dispatch(action)).toBe(action);
    expect(store.getState()).toBe(6);
    
  2. creates the store with the reducer and initial state, and keeps its other methods
    const subscribe = spy();
    const createStore = spy((reducer, state) => ({ getState: () => state, dispatch: (action) => action, subscribe }));
    const reducer = (state) => state;
    const store = applyMiddleware()(createStore)(reducer, 1);
    expect(createStore).toHaveBeenCalledTimes(1);
    expect(createStore).toHaveBeenCalledWith(reducer, 1);
    expect(store.getState()).toBe(1);
    store.subscribe("listener");
    expect(subscribe).toHaveBeenCalledWith("listener");
    
  3. runs the middleware in the order they're listed, around the reducer
    const createStore = (reducer, state) => ({
      getState: () => state,
      dispatch(action) {
        state = reducer(state, action);
        return action;
      },
    });
    const log = [];
    const tag = (name) => () => (next) => (action) => {
      log.push(name + " in");
      const result = next(action);
      log.push(name + " out");
      return result;
    };
    const reducer = (state) => {
      log.push("reducer");
      return state;
    };
    const store = applyMiddleware(tag("a"), tag("b"), tag("c"))(createStore)(reducer, 0);
    store.dispatch({ type: "x" });
    expect(log).toEqual(["a in", "b in", "c in", "reducer", "c out", "b out", "a out"]);
    
  4. a middleware can change an action before passing it on
    const createStore = (reducer, state) => ({
      getState: () => state,
      dispatch(action) {
        state = reducer(state, action);
        return action;
      },
    });
    const double = () => (next) => (action) => next({ ...action, by: action.by * 2 });
    const reducer = (state, action) => (action.type === "add" ? state + action.by : state);
    const store = applyMiddleware(double, double)(createStore)(reducer, 0);
    store.dispatch({ type: "add", by: 1 });
    expect(store.getState()).toBe(4);
    
  5. a middleware that doesn't call next stops the action, and dispatch returns its result
    const createStore = (reducer, state) => ({
      getState: () => state,
      dispatch(action) {
        state = reducer(state, action);
        return action;
      },
    });
    const block = () => (next) => (action) => (action.type === "blocked" ? "stopped" : next(action));
    const reducer = spy((state, action) => (action.type === "inc" ? state + 1 : state));
    const store = applyMiddleware(block)(createStore)(reducer, 0);
    expect(store.dispatch({ type: "blocked" })).toBe("stopped");
    expect(reducer).not.toHaveBeenCalled();
    store.dispatch({ type: "inc" });
    expect(store.getState()).toBe(1);
    
  6. dispatch inside a middleware goes through every middleware again
    const createStore = (reducer, state) => ({
      getState: () => state,
      dispatch(action) {
        state = reducer(state, action);
        return action;
      },
    });
    const seen = [];
    const logger = () => (next) => (action) => {
      seen.push(typeof action === "function" ? "thunk" : action.type);
      return next(action);
    };
    const thunk = ({ dispatch, getState }) => (next) => (action) =>
      typeof action === "function" ? action(dispatch, getState) : next(action);
    const counter = (state, action) => (action.type === "inc" ? state + 1 : state);
    const store = applyMiddleware(logger, thunk)(createStore)(counter, 0);
    const result = store.dispatch((dispatch, getState) => {
      dispatch({ type: "inc" });
      dispatch({ type: "inc" });
      return getState();
    });
    expect(result).toBe(2);
    expect(seen).toEqual(["thunk", "inc", "inc"]);
    
  7. runs the first two layers once, when the store is created
    const createStore = (reducer, state) => ({
      getState: () => state,
      dispatch(action) {
        state = reducer(state, action);
        return action;
      },
    });
    const handle = spy((next) => (action) => next(action));
    const setup = spy(() => handle);
    const store = applyMiddleware(setup)(createStore)((state) => state, 0);
    store.dispatch({ type: "a" });
    store.dispatch({ type: "b" });
    expect(setup).toHaveBeenCalledTimes(1);
    expect(handle).toHaveBeenCalledTimes(1);
    expect(typeof setup.calls[0].args[0].getState).toBe("function");
    expect(typeof setup.calls[0].args[0].dispatch).toBe("function");
    
  8. a middleware that dispatches while it's being set up gets an Error
    const createStore = (reducer, state) => ({
      getState: () => state,
      dispatch(action) {
        state = reducer(state, action);
        return action;
      },
    });
    const eager = ({ dispatch }) => {
      dispatch({ type: "too soon" });
      return (next) => (action) => next(action);
    };
    expect(() => applyMiddleware(eager)(createStore)((state) => state, 0)).toThrow(
      "Dispatching while constructing your middleware is not allowed",
    );
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).