Practical Guide to Redux Thunk: Asynchronous Logic in Redux
Learn how to use Redux Thunk to handle async actions, see a step‑by‑step example, and avoid common pitfalls like state mutation or infinite loops.
02 Aug 2025, 10:49 UTC

Why Use Redux Thunk?
When a component needs to fetch data or perform side‑effects before updating the UI, plain Redux actions are insufficient because they must be plain objects. Redux Thunk solves this by allowing an action creator to return a function instead of an object. That function receives the store’s dispatch and getState methods, enabling you to perform async work and dispatch one or more actions based on the result.
Setting Up Thunk in a Store
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
default:
return state;
}
}
const store = createStore(
counterReducer,
applyMiddleware(thunk) // <‑‑ thunk is wired here
);
Only one line changes compared to a vanilla store: applyMiddleware(thunk). The thunk middleware intercepts dispatched functions and executes them, passing dispatch and getState as arguments.
Writing an Async Thunk
// actionCreator.js
export function incrementAfterDelay(delay = 100) {
return async (dispatch, getState) => {
// Optional: dispatch a loading indicator
dispatch({ type: 'LOADING_START' });
// Simulate an async operation
await new Promise(resolve => setTimeout(resolve, delay));
// After the async work, dispatch the real action
dispatch({ type: 'INCREMENT' });
// Optional: signal that loading finished
dispatch({ type: 'LOADING_END' });
};
}
When incrementAfterDelay is dispatched, the thunk middleware runs the function. Inside the function you can call dispatch multiple times, read the current state via getState, and perform any async logic. The final dispatch({ type: 'INCREMENT' }) updates the reducer, which in turn updates the UI.
Dispatching the Thunk
store.dispatch(incrementAfterDelay(200));
After 200 ms the store’s state will change from { count: 0 } to { count: 1 }. You can subscribe to the store to observe the change:
store.subscribe(() => console.log(store.getState()));
Verification & Testing
- Runtime check: Add
console.log('Thunk started');at the top of the thunk andconsole.log('Thunk finished');after dispatchingINCREMENT. Run the app and confirm the logs appear in order. - Unit test example:
import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { incrementAfterDelay } from './actionCreator'; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); test('incrementAfterDelay dispatches INCREMENT after delay', async () => { const store = mockStore({ count: 0 }); await store.dispatch(incrementAfterDelay(0)); // use 0 for instant test const actions = store.getActions(); expect(actions).toEqual([ { type: 'LOADING_START' }, { type: 'INCREMENT' }, { type: 'LOADING_END' } ]); }); - State mutation guard: In the thunk never mutate
statedirectly; always dispatch actions that reducers handle immutably.
Common Pitfalls and Limitations
1. Direct State Mutation
Because a thunk receives getState, it might be tempting to modify the returned object. That breaks Redux’s immutability contract and can cause UI bugs. Always treat getState() as read‑only and dispatch actions instead.
2. Infinite Dispatch Loops
Dispatching a thunk from within itself without a terminating condition can create an endless loop. For example, calling dispatch(incrementAfterDelay()) at the end of the thunk will re‑enter the thunk repeatedly. Guard with a state flag or condition before dispatching again.
3. Over‑engineering Simple Calls
Redux Thunk is powerful but verbose. For simple API calls that just fetch data and store it, consider RTK Query or a lightweight wrapper. Thunks should be reserved for flows that involve multiple actions, complex error handling, or side‑effects that cannot be expressed declaratively.
4. Lack of Type Safety (TypeScript)
When using TypeScript, the thunk’s signature can become cumbersome. The ThunkAction type from redux-thunk helps, but developers often fall back to any. Prefer explicit types for dispatch and getState to catch errors early.
When to Prefer Alternatives
- RTK Query – Handles caching, polling, optimistic updates, and reduces boilerplate.
- Redux Saga – Better for complex async flows with cancellation or concurrency.
- React Query – Works outside Redux, ideal for data fetching with caching and background refetching.
Practical Checklist
- Install thunk:
npm i redux-thunk(oryarn add redux-thunk). - Apply middleware when creating the store.
- Write action creators that return async functions.
- Never mutate state inside thunks.
- Test the sequence of dispatched actions.
- Consider RTK Query for simple CRUD API patterns.
By following these guidelines, you can confidently integrate Redux Thunk into your application, keeping async logic clear, testable, and maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.