Redux-Thunk Source Code Walkthrough
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 dofetchUser().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
- redux-thunk rewrites
dispatch
so it can accept functions. That’s the entire trick. - For small apps with straightforward data flows, thunk is enough. When async logic spans multiple steps, sagas offer cleaner structure.
- Reading even tiny libraries like thunk sharpens fundamentals—and highlights how elegant a simple solution can be.