Axios with Redux Toolkit and createAsyncThunk

This article explores how the Axios HTTP client integrates with Redux Toolkit's createAsyncThunk API to manage asynchronous data fetching in modern React applications. It covers the core mechanics of dispatching asynchronous actions, executing HTTP requests via Axios, handling lifecycle states (pending, fulfilled, rejected), and normalizing API errors for seamless global state management.


The Roles of Axios and Redux Toolkit

Managing asynchronous API requests in a Redux application involves two primary layers: the transport layer and the state management layer.

When paired, Axios performs the network operation inside the createAsyncThunk payload creator, while RTK automatically propagates the resulting promise state to the Redux store via extraReducers.


Step-by-Step Workflow

1. Defining the Async Thunk

Inside a Redux slice or separate API file, createAsyncThunk wraps an Axios call. The payload creator receives the argument passed to the thunk and a thunkAPI object.

import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

export const fetchUserData = createAsyncThunk(
  'user/fetchUserData',
  async (userId, { rejectWithValue }) => {
    try {
      const response = await axios.get(`/api/users/${userId}`);
      // Axios stores the parsed JSON payload in response.data
      return response.data;
    } catch (error) {
      // Axios error objects store server responses in error.response
      if (!error.response) {
        throw error;
      }
      return rejectWithValue(error.response.data);
    }
  }
);

2. Handling State in the Slice with extraReducers

The slice's extraReducers builder responds to the three promise lifecycle states generated by fetchUserData.

import { createSlice } from '@reduxjs/toolkit';
import { fetchUserData } from './userThunks';

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

export default userSlice.reducer;

3. Dispatching from a Component

When a component calls dispatch(fetchUserData(userId)), the workflow proceeds as follows:

  1. fetchUserData.pending is immediately dispatched, updating loading to true.
  2. Axios executes the HTTP request.
  3. If the request succeeds (HTTP 2xx), the promise resolves, and fetchUserData.fulfilled is dispatched with response.data as action.payload.
  4. If the request fails (HTTP 4xx/5xx or network failure), the promise rejects. Using rejectWithValue, fetchUserData.rejected is dispatched with the custom error payload.

Key Integration Considerations