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.

June 27, 20264 min read1 / 3

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.

JavaScript
// 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

Bash
npm install redux-thunk

Wire it into the store as middleware in src/store/index.js:

JavaScript
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:

JavaScript
export const FETCH_TASKS = 'FETCH_TASKS';

Write the thunk action creator in actions/tasks.js:

JavaScript
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:

JavaScript
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:

JSX
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).

Redux Thunk async flow: component dispatches thunk, Thunk calls it with dispatch, HTTP response triggers a plain action 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

  1. Plain Redux actions must be objects. Thunk extends the contract to allow action creators to return functions. Those functions receive dispatch and can dispatch real actions at any later point.
  2. Wire Thunk via applyMiddleware(thunk) inside composeWithDevTools() in the store setup. Without this, dispatching a function throws an error.
  3. 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