Redux-Thunk Source Code Walkthrough

· 3 min read · 615 Words · -Views -Comments

redux-thunk is a middleware that lets Redux actions return functions. That makes async flows reusable—dispatch can now trigger an async operation and decide when to fire the real action.

Without thunk, every component that needs fetchUser() must do fetchUser().then(...) and dispatch manually. With thunk we wrap that logic once and dispatch the wrapper.

Adding Thunk to a Project

const middleWares = [sagaMiddleware, thunk, routerMiddleware(history)];
const store = createStore(
  rootReducer(history),
  compose(applyMiddleware(...middleWares), reduxDevtools)
);

applyMiddleware stitches thunk (and any other middleware) into Redux. To understand thunk’s source, it helps to glance at Redux internals.

applyMiddleware in Redux

The current Redux source is written in TypeScript. Relevant snippet:

export default function applyMiddleware(
  ...middlewares: Middleware[]
): StoreEnhancer<any> {
  return (createStore: StoreCreator) => <S, A extends AnyAction>(
    reducer: Reducer<S, A>,
    ...args: any[]
  ) => {
    const store = createStore(reducer, ...args);
    let dispatch: Dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      );
    };

    const middlewareAPI: MiddlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args)
    };
    const chain = middlewares.map(middleware => middleware(middlewareAPI));
    dispatch = compose<typeof dispatch>(...chain)(store.dispatch);

    return {
      ...store,
      dispatch
    };
  };
}

compose is the standard functional helper that pipes functions from right to left:

export default function compose(...funcs: Function[]) {
  if (funcs.length === 0) {
    return <T>(arg: T) => arg;
  }

  if (funcs.length === 1) {
    return funcs[0];
  }

  return funcs.reduce((a, b) => (...args: any) => a(b(...args)));
}

The middleware chain wraps the original store.dispatch. Later, when createStore sees an enhancer, it invokes it:

export default function createStore<
  S,
  A extends Action,
  Ext = {},
  StateExt = never
>(
  reducer: Reducer<S, A>,
  preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
  enhancer?: StoreEnhancer<Ext, StateExt>
) {
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.');
    }

    return enhancer(createStore)(reducer, preloadedState as PreloadedState<S>);
  }

  // ... regular createStore logic
}

So when thunk is part of middlewares, it becomes one of the functions in chain and can intercept dispatch calls.

redux-thunk Repository Layout

Source: https://github.com/reduxjs/redux-thunk

The project is tiny—src/index.js holds the implementation, alongside TypeScript definition files, tests (using mocha/chai), and a standard toolchain (Babel, ESLint, Travis CI).

├── src
│   ├── index.d.ts
│   └── index.js
├── test
│   ├── index.js
│   └── typescript.ts
└── ... other project config files

Thunk Source Code

Only 12 lines:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

It’s a curried function. If the dispatched “action” is a function, thunk invokes it, passing dispatch, getState, and an optional extra argument. Otherwise it passes the action along to the next middleware (or the original dispatch).

Thunk vs. Saga

redux-saga is another middleware option. It watches for plain-object actions and coordinates side effects with generators. In contrast, thunk changes what dispatch accepts—functions instead of plain objects.

Both can handle async flows, but they suit different needs:

  • Thunk keeps things simple for basic async calls. Each thunk decides when to dispatch.
  • Saga separates concerns with watchers and effects—better for complex orchestration, retries, cancellation, or long-lived workflows.

Many teams (including mine) eventually migrate to saga when thunks grow into nested callbacks.

Takeaways

  1. redux-thunk rewrites dispatch so it can accept functions. That’s the entire trick.
  2. For small apps with straightforward data flows, thunk is enough. When async logic spans multiple steps, sagas offer cleaner structure.
  3. Reading even tiny libraries like thunk sharpens fundamentals—and highlights how elegant a simple solution can be.

References

Authors
Developer, digital product enthusiast, tinkerer, sharer, open source lover