DAY 52: Mastering Async in Redux Toolkit – Custom API Middleware, Redux Thunk & createAsyncThunk | ReactJS

Ritik KumarRitik Kumar
7 min read

🚀 Introduction

Welcome to Day 52 of my Web Development Journey!
After building a strong foundation in HTML, CSS, and JavaScript, I’ve been diving deep into ReactJS — a powerful library for creating dynamic and interactive user interfaces.

So far, I’ve explored state management in React using useState, Context API, useReducer, and Zustand. Recently, I started learning Redux, one of the most popular and robust libraries for managing state in modern web applications.

Over the past few days, after strengthening my understanding of the core concepts of Redux, I moved on to Redux Toolkit (RTK) — the official, modern, and recommended way to use Redux. Here’s what I learned and practiced:

  • Implemented a Custom API Middleware for data fetching
  • Explored Redux Thunk and understood its internal implementation
  • Learned and practiced createAsyncThunk to generate thunk action creators

📂 You can check out my complete Redux learning progress in my GitHub repository.

👉 Feel free to follow me on Twitter for real-time updates and coding insights.

I’m documenting this journey publicly to stay consistent and to share my learnings. Whether you’re just starting with React or looking to strengthen your knowledge of state management, I hope this blog provides value to your journey!

📅 Here’s What I Covered Over the Last 3 Days

Day 49

  • Implemented a Custom API Middleware for Data Fetching

Day 50

  • Explored Redux Thunk
  • Understood its internal implementation
  • Practiced Redux Thunk with action creators that return a thunk function as an action

Day 51

  • Learned createAsyncThunk to generate thunk action creators
  • Explored how it handles success, error, and loading internally

Now, let’s break down each of these concepts with explanations and implementations 👇


1. Implementing a Custom API Middleware for Data Fetching

When working with Redux, handling API calls directly inside components often leads to cluttered code and repetition. A cleaner approach is to use Middleware — which allows us to intercept actions before they reach the reducer.

I implemented a Custom API Middleware to handle all data-fetching logic in a centralized way.

Problem:

We want to:

  • Trigger an API call by dispatching a single action.
  • Handle loading, success, and error states without writing repetitive code in every component.
  • Keep all API-related logic separate from UI components and reducers.

Middleware Implementation:

export const apiMiddleware = (store) => (next) => (action) => {
  const BASE_URL = "https://fakestoreapi.com";

  if (action.type === "api/fetchData") {
    next(action);

    const { url, onStart, onSuccess, onError } = action.payload;

    // Dispatch loading action
    store.dispatch({ type: onStart });

    // Make API call
    fetch(`${BASE_URL}/${url}`)
      .then((res) => res.json())
      .then((data) =>
        store.dispatch({
          type: onSuccess,
          payload: data,
        })
      )
      .catch(() => store.dispatch({ type: onError }));
  } else {
    next(action);
  }
};

// Action creator
export const fetchData = (payload) => {
  return { type: "api/fetchData", payload };
};

How It Works?

  • Intercept API Action → The middleware listens for any action with type "api/fetchData".
  • Trigger Loading State → Dispatches onStart before making the API request.
  • Fetch Data → Calls the API using fetch.
  • Handle Response → Dispatches onSuccess with the data if successful, or onError if the request fails.
  • Pass Other Actions → If the action is not "api/fetchData", it simply passes the action to the next middleware/reducer.

Example Usage:

Suppose we want to fetch products:

dispatch(
  fetchData({
    url: "products",
    onStart: "products/fetchStart",
    onSuccess: "products/fetchSuccess",
    onError: "products/fetchError",
  })
);

This will:

  • Dispatch products/fetchStart → to set the loading state.
  • Fetch data from https://fakestoreapi.com/products.
  • On success → Dispatch products/fetchSuccess with the data.
  • On error → Dispatch products/fetchError.

With just one dispatch, we cover the full API lifecycle.

Benefits:

  • Reusability → No need to write API calls in every component.
  • Separation of Concerns → Middleware handles API logic, reducers handle state updates.
  • Scalability → Adding new API endpoints only requires dispatching fetchData with different payloads.

2. Redux Thunk

Redux by default only allows synchronous actions (plain objects). But real-world apps often need to fetch data from APIs, which is asynchronous.

This is where Redux Thunk comes in. It’s a middleware that lets us write action creators that return a function instead of an object. That function can contain asynchronous logic, and it receives dispatch (and getState) as arguments.

Internal Implementation:

At its core, Redux Thunk looks like this

const thunkMiddleware = ({ dispatch, getState }) => (next) => (action) => {
  if (typeof action === "function") {
    return action(dispatch, getState); // execute the function
  }
  return next(action); // if normal action, pass it along
};
  • If the action is a function → it executes it, giving access to dispatch and getState.
  • If it’s a plain object → it forwards the action to reducers as usual.

Example (Fetching Cart Items):

Here’s how we can use Redux Thunk in our project:

// cartSlice actions
const { loadCartItems, fetchCartItems, fetchCartItemsError } = cartSlice.actions;

// Thunk Action Creator
export const fetchCartItemsData = () => (dispatch) => {
  dispatch(fetchCartItems()); // set loading
  fetch(`https://fakestoreapi.com/carts/1`)
    .then((res) => res.json())
    .then((data) => dispatch(loadCartItems(data))) // success
    .catch(() => dispatch(fetchCartItemsError())); // error
};

And we consume it like this:

dispatch(fetchCartItemsData());

Here’s what happens step by step:

  1. Dispatch fetchCartItems() → sets loading state.
  2. Make an API request to Fake Store API.
  3. On success → dispatch loadCartItems(data) with the payload.
  4. On failure → dispatch fetchCartItemsError().

Problem with Raw Thunk:

While Thunk is powerful, using raw thunks directly has some drawbacks:

  • Boilerplate → We need to manually dispatch loading, success, and error every time.
  • Inconsistent Patterns → Every developer might implement thunks differently, leading to code inconsistency.
  • Error Handling → Needs to be repeated across multiple thunks.

That’s why Redux Toolkit introduced createAsyncThunk, which automatically handles these repetitive patterns and makes async logic cleaner.


3. createAsyncThunk in Redux Toolkit

Manually handling loading, success, and error with raw thunks can quickly become repetitive.
To solve this, Redux Toolkit provides createAsyncThunk, which simplifies async logic by generating action types and handling the lifecycle automatically.

Example:

// Thunk Action Creator
export const fetchCartItemsData = createAsyncThunk(
  "cart/fetchCartItems",
  async () => {
    try {
      const res = await fetch(`https://fakestoreapi.com/carts/1`);
      return res.json();
    } catch (err) {
      throw err;
    }
  }
);

// Slice with extraReducers
extraReducers: (builder) => {
  builder
    .addCase(fetchCartItemsData.pending, (state) => {
      state.loading = true;
      state.error = "";
    })
    .addCase(fetchCartItemsData.fulfilled, (state, action) => {
      state.loading = false;
      state.list = action.payload.products;
      state.error = "";
    })
    .addCase(fetchCartItemsData.rejected, (state, action) => {
      state.loading = false;
      state.error = action.payload || "Something went wrong";
    });
},

How It Works Internally?

  • Dispatch Thunk → When we call dispatch(fetchCartItemsData()), Redux Toolkit internally dispatches a pending action.

  • Async Execution → The async function runs (fetch call in this case).

  • Success Case → If resolved, it automatically dispatches the fulfilled action with the response data.

  • Error Case → If rejected, it automatically dispatches the rejected action with the error.

This means we don’t need to manually dispatch three actions (loading, success, error) anymore — createAsyncThunk generates and manages them for us.

Benefits of createAsyncThunk:

  • Less Boilerplate → No need to write repetitive loading/success/error dispatch.
  • Consistency → Every async action follows the same pattern.
  • Error Handling → Automatically handled with rejected case.
  • Integration with Immer → State updates remain clean and immutable inside reducers.

4. What’s Next

I’m excited to keep growing and sharing along the way! Here’s what’s coming up:

  • Posting new blog updates every 3 days to share what I’m learning and building.
  • Diving deeper into Data Structures & Algorithms with Java — check out my ongoing DSA Journey Blog for detailed walkthroughs and solutions.
  • Sharing regular progress and insights on X (Twitter) — feel free to follow me there and join the conversation!

Thanks for being part of this journey!


5. Conclusion

In this blog, we explored three powerful approaches to handle asynchronous logic in Redux:

  • Custom API Middleware → Complete control and flexibility to intercept API actions.
  • Redux Thunk → A widely used middleware that allows writing async logic inside action creators.
  • createAsyncThunk (RTK) → The modern, recommended way that eliminates boilerplate and enforces a consistent pattern for handling async requests.

Each approach builds on top of the previous one — from raw middleware to the abstractions provided by Redux Toolkit.
While custom middleware gives us ultimate flexibility, Redux Thunk simplifies async logic, and createAsyncThunk takes it one step further by standardizing loading, success, and error states automatically.

👉 If you’re starting fresh with Redux, always prefer Redux Toolkit and createAsyncThunk, as it makes our code cleaner, more maintainable, and less error-prone.

With these tools in my arsenal, I'm well-prepared to handle real-world API calls, side effects, and state management in modern React applications.

👉 Up next: I’ll be diving into RTK Query, Redux Toolkit’s powerful data-fetching and caching solution, to simplify API calls even further. Stay tuned!

Thanks for reading — and if you’re also learning Redux Toolkit, I hope this blog helps you speed up your journey

1
Subscribe to my newsletter

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

Written by

Ritik Kumar
Ritik Kumar

👨‍💻 Aspiring Software Developer | MERN Stack Developer.🚀 Documenting my journey in Full-Stack Development & DSA with Java.📘 Focused on writing clean code, building real-world projects, and continuous learning.