Imagine trying to bake a cake (your application state) and needing ingredients (data from an API). But the ingredients aren't instantly available—you’ve sent someone to the store, and they’ll be back later. You need a way to wait for the ingredients, and only once they arrive, add them to your cake. That’s what thunks help you do in Redux—they allow you to delay the action until the data is ready.
When building web apps that interact with servers, you'll often request data (ingredients) and need to update your application state (cake batter) once the data returns. Ideally, we want a consistent pattern for how our state updates. Instead of placing asynchronous code in components, we centralize it in action creators. This makes code more reusable, testable, and maintainable.
That’s where thunks come in—they are special action creators that allow async operations before dispatching a real action.
Normally, action creators return an object—like placing a pizza order and getting a receipt. A thunk, however, returns a function—like placing a pizza order and receiving a callback to notify you when it’s delivered. Until the callback is executed, nothing changes.
const thunkActionCreator = () => (dispatch) => {
dispatch({
type: 'RECEIVE_MESSAGE',
message: 'This will be dispatched immediately.'
});
setTimeout(() => {
dispatch({
type: 'RECEIVE_MESSAGE',
message: 'This will be dispatched 1 second later.'
});
}, 1000);
};
Here, the first message dispatches immediately, and the second waits 1 second. But this function would break unless we have middleware—like a bouncer at a club—who checks if an action is a function and, if so, knows what to do with it.
The thunk middleware intercepts function-based actions and invokes them with dispatch and getState.
const thunk = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
You can write your own middleware or use the one from the redux-thunk package.
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
const configureStore = (preloadedState = {}) => {
return createStore(
rootReducer,
preloadedState,
applyMiddleware(thunk, logger) // thunk before logger
);
};
export default configureStore;
Order matters: thunk comes first to handle async actions before the logger tries to print them.
Let’s explore a real-world example—fetching fruit data from a server.
export const RECEIVE_FRUITS = 'RECEIVE_FRUITS';
export const fetchFruits = () => async (dispatch) => {
const res = await fetch('/fruits');
const data = await res.json();
res.data = data;
if (res.ok) {
dispatch(receiveFruits(data.fruits));
} else {
throw res;
}
};
const receiveFruits = (fruits) => {
return {
type: RECEIVE_FRUITS,
fruits,
};
};
This thunk action creator fetches data from the server. Once the data arrives (like the fruit delivery truck), it dispatches a regular action to update the Redux store with the fruit list.
Thunks can be dispatched just like normal actions using React Redux’s useDispatch hook.
import { useDispatch } from 'react-redux';
import { useEffect } from 'react';
import { fetchFruits } from './store/fruitActions';
function FruitComponent() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchFruits());
}, [dispatch]);
return <div>Loading fruits...</div>;
}
Thunks allow Redux action creators to delay dispatching until async code is resolved. They’re like phone calls to the server that only trigger state updates once the answer arrives. This pattern keeps async logic out of components and centralizes data flow in your Redux layer—making apps more robust and maintainable.
Happy thunking!