Redux Thunk: Async Actions and HTTP Requests
Redux reducers must be synchronous. Redux Thunk adds middleware that lets action creators return functions, making async operations like HTTP requests possible inside the Redux flow.
The task manager reads and writes local state correctly. Every task is stored in Redux state initialized from initialTasks. What is missing: actual data from a server. Loading data from an API is an asynchronous operation. By default, Redux cannot handle that.
This post explains why, and how Redux Thunk fixes it.
The Problem with Async in Redux
By definition, a Redux action must be a plain object. Dispatch receives that object, passes it to the reducer, and the reducer returns new state. All synchronous. All predictable.
An HTTP request breaks that model. You dispatch, the request takes 200ms to 2 seconds, and then you need to dispatch again with the response data. The plain-object contract cannot accommodate a "dispatch later when the response arrives" pattern.
// This fails in plain Redux: no access to dispatch from inside a returned function
export const fetchTasks = () => {
fetch('http://localhost:7000/tasks')
.then(res => res.json())
.then(data => dispatch({ type: FETCH_TASKS, payload: data })); // no access to dispatch
};Redux Thunk solves this by allowing action creators to return a function. When the store's middleware detects that the dispatched value is a function (not an object), it calls that function with (dispatch, getState). Inside it, you can do anything, including making HTTP requests, and dispatch real actions when the data arrives.
Installation
npm install redux-thunkWire it into the store as middleware in src/store/index.js:
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import allReducers from '../reducers';
const store = createStore(
allReducers,
composeWithDevTools(applyMiddleware(thunk))
);
export default store;applyMiddleware(thunk) registers Thunk as middleware. Every dispatched value now passes through Thunk first. If it is a function, Thunk calls it. If it is a plain object, Thunk passes it through unchanged.
The Thunk Pattern
Add a FETCH_TASKS action type to constants/action-types.js:
export const FETCH_TASKS = 'FETCH_TASKS';Write the thunk action creator in actions/tasks.js:
export const fetchTasks = () => async (dispatch) => {
const response = await fetch('http://localhost:7000/tasks');
const data = await response.json();
dispatch({ type: actionTypes.FETCH_TASKS, payload: data });
};The outer function fetchTasks is the action creator. It returns the inner async function. That is the thunk. Redux Thunk intercepts it, calls it with dispatch, and the HTTP request runs. When the response arrives, a real action with a payload is dispatched.
Add the reducer case:
case actionTypes.FETCH_TASKS:
return action.payload;Dispatch the thunk from Tasks.js inside a useEffect that runs once on mount. These lines go inside the Tasks component function, right after the useSelector call:
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import actions from '../../actions';
export default function Tasks() {
const tasks = useSelector((state) => state.tasks);
const dispatch = useDispatch();
useEffect(() => {
dispatch(actions.fetchTasks());
}, [dispatch]);
// ... rest of the component
}The [dispatch] dependency is correct. dispatch is stable, so this useEffect runs only once on mount. The tasks load from the server, flow through Thunk, hit the reducer, and useSelector picks up the new state. The store setup with composeWithDevTools must already be in place before adding applyMiddleware(thunk).
ExpandRedux Thunk async flow: component dispatches thunk, Thunk calls it with dispatch, HTTP response triggers a plain action
What "Thunk" Means
The name "thunk" is an old compiler term for a deferred computation. In Redux Thunk's usage, the thunk is the function returned by the action creator. It is the deferred work: a computation that will run when Redux Thunk calls it.
The flow: component dispatches fetchTasks() → Redux Thunk detects a function → calls the function with (dispatch, getState) → HTTP request runs → response arrives → inner dispatch fires → reducer handles a plain action object → state updates → component re-renders.
The Essentials
- Plain Redux actions must be objects. Thunk extends the contract to allow action creators to return functions. Those functions receive
dispatchand can dispatch real actions at any later point. - Wire Thunk via
applyMiddleware(thunk)insidecomposeWithDevTools()in the store setup. Without this, dispatching a function throws an error. - The thunk pattern: outer function returns an async inner function that takes
(dispatch, getState), performs async work, and dispatches a plain action object when the data arrives.
Further Reading and Watching
- Redux Thunk on GitHub: official docs and source (the implementation is 14 lines)
- Redux Async Logic and Side Effects: the official guide on async patterns in Redux
store/index.json GitHub: the final store setup showing Thunk wired into applyMiddlewareactions/tasks.json GitHub: the complete async action creators using the thunk pattern
Keep reading