In our previous article, we explored what Redux “actually“ is. But now it is time to get into more deeper waters!!
In Redux, middleware is a way to extend Redux's capabilities. It's a function that sits between the action being dispatched and the reducer that handles the action. Middleware can be used for logging, handling asynchronous actions, modifying actions, and more.
Think of middleware as a pipeline through which every action must pass before reaching the reducer.
In simple terms, Redux middleware is like a middleman that sits between the action being dispatched and the reducer that updates the state. It allows you to "intercept" the action and do something with it before it reaches the reducer.
Middleware lets you handle things that aren’t straightforward in Redux, like async actions (e.g., API calls). Normally, Redux expects plain action objects to be dispatched, but with middleware (like Redux Thunk), you can dispatch functions instead. This lets you pause, make API calls, or do any side effects, and then continue when you're ready.
Some of the popular Redux Middleware Libraries are:
redux-thunk
: Allows you to write action creators that return a function (instead of an action object) to handle asynchronous logic.redux-saga
: Helps manage side effects, especially complex asynchronous flows, using a more declarative approach.
As in above image, we will be exploring redux-thunk
; because it's simpler and more commonly used for handling async actions like API calls.
What’s a Thunk?
A thunk in programming (and specifically in Redux) is a function that delays the evaluation of an operation. It's basically a function that wraps an expression to be evaluated later.
In the context of Redux Thunk, a thunk is a function that returns another function, instead of an action object. This second function can contain asynchronous logic, like making API requests, and then dispatch an action once the request completes.
Let’s have a crack at it by practicing on an example:
Let us start by defining the Action Types. Action Types will set the tone for us, what action to be invoked, based on which certain activity will be performed.
export const FETCH_DATA_REQUEST = 'FETCH_DATA_REQUEST';
export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS';
export const FETCH_DATA_FAILURE = 'FETCH_DATA_FAILURE';
These will be our actions. The first action will work when we start to fetch the data. If data fetch operation turns out to be successful, then {AS THE NAMES OF ACTIONS SUGGEST} will be invoked accordingly.
These are just constants that we will use to tell our reducer (which I will explain later) what exactly is happening behind the scenes.
Now lets move on to Action creators; which are functions that create actions. Usually, these functions return an object with a type and payload (data). But when using redux-thunk
, action creators can return functions instead of objects. These functions can contain asynchronous logic, like fetching data from a server.
import { FETCH_DATA_REQUEST, FETCH_DATA_SUCCESS, FETCH_DATA_FAILURE } from './actionTypes';
export const fetchData = () => {
return async dispatch => {
dispatch({ type: FETCH_DATA_REQUEST }); // Dispatch an action to say that data fetching has started
try{
const response = await fetch('https://jsonplaceholder.typicode.com/posts'); // Fetch data from an external API
const data = await response.json();
dispatch({ type: FETCH_DATA_SUCCESS, payload: data }); // Dispatch success action with the fetched data
} catch(error){
dispatch({ type: FETCH_DATA_FAILURE, error: error.message }); // If there's an error, dispatch a failure action with the error message
}
}
}
The main task of Action Creators is to Dispatch the actions. Dispatching Actions uses dispatch()
to send actions to the Redux store. First, it dispatches FETCH_DATA_REQUEST
to indicate the start of the data fetch (so we can show a loading spinner, for example). Then it fetches the data from a fake API. If the data is fetched successfully, it dispatches FETCH_DATA_SUCCESS
and attaches the fetched data (payload: data
) to the action. If the fetch fails, it dispatches FETCH_DATA_FAILURE
with an error message.
Next up we have a reducer.
A reducer is a function that takes the current state and an action, and returns the new state based on that action. It "reduces" the app's state by handling the action and modifying the state accordingly.
import { FETCH_DATA_REQUEST, FETCH_DATA_SUCCESS, FETCH_DATA_FAILURE } from '../actions/actionTypes';
const initialState = {
data: [], // for fetched data
loading: false, // implies that we are not loading up any data as of yet
error: '', // can be used to accomodate error messages (if things go south)
};
const dataReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_DATA_REQUEST:
// If the data fetching has started, set loading to true
return { ...state, loading: true };
case FETCH_DATA_SUCCESS:
// If data was fetched successfully, update the state with the data and set loading to false
return { ...state, loading: false, data: action.payload };
case FETCH_DATA_FAILURE:
// If something went wrong, set loading to false and store the error message
return { ...state, loading: false, error: action.error };
default:
// If none of the action types match, return the current state unchanged
return state;
}
};
export default dataReducer;
We start with an initial state. Based on the action type (FETCH_DATA_REQUEST
, FETCH_DATA_SUCCESS
, or FETCH_DATA_FAILURE
), the state gets updated. You may refer the comments in above code to understand the purpose and action effect for each action.
Now it is time for Store.
The Redux store is where all the app’s state is kept. It's the central place that holds the global state for your app. We also apply the redux-thunk middleware here, so our action creators can return functions for asynchronous logic.
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import dataReducer from './reducers/dataReducer';
// ⭐ Combine all reducers into a single root reducer
const rootReducer = combineReducers({
data: dataReducer // NOTE : We only have one reducer, dataReducer, for now
});
// ⭐ Create the store with the root reducer and thunk middleware
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;
The purpose of combineReducers
is that we combine different reducers into a single rootReducer
. Even if you have just one reducer, you use this function in case you add more later. More important guest of honour is createStore
, which creates the Redux store, passing the rootReducer
and applying the redux-thunk
middleware using applyMiddleware
.
For finale preparations, we need to set up App.js like this:
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchData } from './actions/dataActions';
function App() {
// Hook to dispatch actions to Redux
const dispatch = useDispatch();
// Hook to access the Redux state (data, loading, error) from the reducer
const { loading, data, error } = useSelector((state) => state.data);
// Fetch data when the component mounts (runs only once)
useEffect(() => {
dispatch(fetchData());
}, [dispatch]);
return (
<div style={{ textAlign: 'center' }}>
<h1>Data Fetching with Redux Thunk</h1>
{loading && <p>Loading...</p>}
{error && <p>Error: {error}</p>}
<ul>
{data.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
export default App;
We have used useDispatch hook, a hook provided by Redux to dispatch actions (in our case, fetchData
). We have also used another hook which is useSelector, a hook to access the Redux state in your component. Here, we're accessing loading
, data
, and error
from the data
part of the state (in our case handled by dataReducer
). Oh yeah and lets not rue the side-effects out of action, so we have also used useEffect in here, we have used it here to trigger the fetchData
action when the component first mounts (I mean “loads”).
Understanding middleware and async handling with Redux helps you build scalable, efficient applications that manage data and side effects cleanly. This knowledge will be valuable as we continue to explore more complex React-Redux scenarios in further blog series.
Until then ciao