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.

  • Axios (Transport Layer): Axios is a promise-based HTTP client responsible for sending network requests, setting headers, serializing request data, parsing JSON responses automatically, and intercepting request/response lifecycles.
  • Redux Toolkit & createAsyncThunk (State Layer): createAsyncThunk is an abstraction provided by Redux Toolkit (RTK) that accepts an action type string and a "payload creator" callback function. It automatically generates and dispatches Redux action types based on the returned promise's lifecycle: pending, fulfilled, or rejected.

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

  • Error Serialization: Standard JavaScript Error objects cannot be properly serialized into Redux actions. Axios errors contain non-serializable properties (like the request configuration). Always extract only serializable data (such as error.response.data or error.message) using thunkAPI.rejectWithValue.
  • Axios Interceptors: Global Axios interceptors can be configured for authentication tokens or refreshing tokens. These run independently of Redux, ensuring createAsyncThunk receives ready-to-use responses without duplicating authentication logic inside every thunk.
  • Cancellation with AbortController: createAsyncThunk exposes an abort signal via thunkAPI.signal. This can be passed directly to Axios's signal configuration to cancel ongoing HTTP requests when a thunk is aborted or when a component unmounts.