Day 9: ReactJS - Redux Middleware and Asynchronous Actions

Saket KhopkarSaket Khopkar
6 min read

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.

Structuring Redux for Use in React: An Overview

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

0
Subscribe to my newsletter

Read articles from Saket Khopkar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Saket Khopkar
Saket Khopkar

Developer based in India. Passionate learner and blogger. All blogs are basically Notes of Tech Learning Journey.