How to Update Redux Thunk in React

This article provides a straightforward guide on how to update and implement Redux Thunk in a React application. You will learn how to transition from legacy, manual Redux Thunk configurations to the modern Redux Toolkit standard, utilizing configureStore and createAsyncThunk for efficient asynchronous state management.

Step 1: Upgrade to Redux Toolkit

In legacy React applications, Redux Thunk had to be installed as an independent package and manually applied as middleware. The modern way to update and use Redux Thunk is by adopting Redux Toolkit (@reduxjs/toolkit). Redux Toolkit includes Redux Thunk by default, eliminating the need for manual middleware configuration.

To update your project, install the latest version of Redux Toolkit and React-Redux:

npm install @reduxjs/toolkit react-redux

Step 2: Update the Store Configuration

Historically, you configured Thunk using applyMiddleware(thunk) inside Redux’s createStore. With Redux Toolkit, you replace createStore with configureStore. This automatically enables the Redux Thunk middleware under the hood.

Here is how to update your store configuration:

import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';

const store = configureStore({
  reducer: {
    user: userReducer,
  },
});

export default store;

Step 3: Define Asynchronous Logic with createAsyncThunk

Instead of writing manual creator functions that return a function, use Redux Toolkit’s createAsyncThunk. This standardizes how promises and asynchronous requests are handled.

createAsyncThunk accepts an action type string and a payload creator callback function that returns a promise:

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

// The thunk handles the asynchronous request
export const fetchUserById = createAsyncThunk(
  'user/fetchById',
  async (userId) => {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    return await response.json();
  }
);

Step 4: Handle Lifecycle Actions in a Slice

createAsyncThunk automatically generates action creators for three promise lifecycles: pending, fulfilled, and rejected. You must update your slice reducer to handle these actions using the extraReducers builder callback:

const userSlice = createSlice({
  name: 'user',
  initialState: { data: null, loading: false, error: null },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserById.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchUserById.fulfilled, (state, action) => {
        state.loading = false;
        state.data = action.payload;
      })
      .addCase(fetchUserById.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  },
});

export default userSlice.reducer;

Step 5: Dispatch the Thunk in React Components

To trigger the updated Redux Thunk inside your functional React components, use the useDispatch hook from react-redux. Pass the payload argument directly into the generated thunk function:

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchUserById } from './userSlice';

const UserProfile = ({ userId }) => {
  const dispatch = useDispatch();
  const { data, loading, error } = useSelector((state) => state.user);

  useEffect(() => {
    dispatch(fetchUserById(userId));
  }, [dispatch, userId]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return data ? <div><h1>{data.name}</h1></div> : null;
};

export default UserProfile;