Simplify Async Actions in Redux with createAsyncThunk

Simplify Async Actions in Redux with createAsyncThunk

ยท

5 min read

In the previous article of this Redux series, we learned about the basics of Redux and how to write a basic Redux store. We also learned about the use of reducers to manage the state of our application.

However, as our application grows in complexity, we might need to write more complex actions that deal with asynchronous operations such as fetching data from an API or sending a request to a server. Writing such actions can be tedious, error-prone, and can lead to code duplication.

Enter createAsyncThunk, a utility function provided by the official Redux Toolkit that simplifies the process of handling asynchronous actions in Redux.

What is createAsyncThunk?

createAsyncThunk is a utility function that helps to create a tRedux thunks , thunks are the functions that delay the tasks and that handle asynchronous actions in a standardized way. It allows you to define a single action creator function that dispatches multiple actions based on the state of the asynchronous operation.

Here's how it works:

  1. Dispatch an initial "pending" action to indicate that the asynchronous operation has started.

  2. Perform the asynchronous operation inside the thunk function.

  3. If the operation is successful, dispatch a "fulfilled" action with the result of the operation.

  4. If the operation fails, dispatch a "rejected" action with the error message.

Here's an example of how we can use createAsyncThunk to fetch data from an API:

import { createAsyncThunk } from '@reduxjs/toolkit';
import { fetchUserData } from '../api/user';

export const getUserData = createAsyncThunk('user/getUserData', async () => {
  try {
    const response = await fetchUserData();
    return response.data;
  } catch (error) {
    return Promise.reject(error.message);
  }
});

In the code above, we're defining a new async thunk function called getUserData. It dispatches three different actions in sequence โ€“ "pending", "fulfilled", and "rejected" โ€“ based on the state of the asynchronous operation.

How to use createAsyncThunk

Using createAsyncThunk is simple. You define the name of the action, the async function to be performed by the thunk, and any additional options required for handling the operation.

Here's the basic syntax of the createAsyncThunk method:

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

const myAction = createAsyncThunk(
  'mySlice/myAction',
  async (arg1, arg2, { dispatch, getState, extra }) => {
    // Async function body
  },
  {
    condition: (arg1, arg2) => shouldPerformOperation(arg1, arg2),
  }
);

The first argument to createAsyncThunk is a string that represents the action type name for our async operation. The second argument is the async function body that performs the required async operation.

We can also pass an optional third argument to createAsyncThunk specify configuration options for our async action. One such option is, which can be used to prevent unnecessary re-fetching of data.

Real-life Use Case

In a real-life application, you can use createAsyncThunk for any async operation such as reading and writing data from a database, fetching data from an API, or sending a request to a server.

For example, you can use createAsyncThunk to fetch user data from an API and update the Redux store with the result:

import { createAsyncThunk } from '@reduxjs/toolkit';
import { fetchUserData } from '../api/user';

export const getUserData = createAsyncThunk('user/getUserData', async () => {
  try {
    const response = await fetchUserData();
    return response.data;
  } catch (error) {
    return Promise.reject(error.message);
  }
});

// Slice
import { createSlice } from '@reduxjs/toolkit';
import { getUserData } from './userActions';

const userSlice = createSlice({
  name: 'user',
  initialState: { data: null, isLoading: false, error: null },
  reducers: {},
  extraReducers: {
    [getUserData.pending]: (state) => {
      state.isLoading = true;
    },
    [getUserData.fulfilled]: (state, { payload }) => {
      state.data = payload;
      state.isLoading = false;
      state.error = null;
    },
    [getUserData.rejected]: (state, action) => {
      state.error = action.payload;
      state.isLoading = false;
    },
  },
});

export default userSlice.reducer;

In the code above, we're defining a new AsyncThunk called getUserData, which fetches user data from an API and returns the result. We're then using it in a Redux slice by defining what should be done for each thunk action type of pending, fulfilled and rejected. With this, we can easily fetch user data and update it in the store.

Compare to the traditional slice where we also export the actions in createAsyncThunk we need not export the actions because async method will work as the action in the above case it is getUserData .

Use the above slice in the store.jsx

import { configureStore } from '@reduxjs/toolkit';
import getUserData from '../src/slice/userSlice ';


export const store = configureStore({
  reducer: {
    content: userSlice ,
  },
});

Use the getUserData in the component

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

function ComponentWithRedux() {
  const dispatch = useDispatch();

  useEffect(() => {
    // We directly call the fetch function instead of using actions
    dispatch(getUserData());
  }, [dispatch]);

  const contents = useSelector((state) => state.content.contents);
  const isLoading = useSelector((state) => state.content.isLoading);
  const error = useSelector((state) => state.content.error);

  if (isLoading) {
    return (
      <div className="text-center">
        {' '}
        <Spinner animation="border" variant="success" />
      </div>
    );
  }

  if (error) {
    return error;
  }

  return (
    <div className="d-flex flex-wrap justify-content-center gap-4 mt-5">
      {contents.map((content) => (
       // Use the data as per the requirement
      ))}
    </div>
  );
}

export default ComponentWithRedux;

Here we have called our async function i.e is getUserData and used the current state of the async function using useSelector hook.h

Small project using createAsyncThunk

I created this small application using createAsyncThunk.

๐Ÿง‘โ€๐Ÿ’ป Live Code

Conclusion

createAsyncThunk is a powerful utility function that simplifies the process of handling async actions in Redux. It standardizes the way we write async actions, simplifies the code, and removes common mistakes which can be made when writing these types of actions. By using it, we can simplify our Redux code, making our code more readable and maintainable.

In the next blog, we will discuss How to use RTK Query, a data fetching and caching solution designed for Redux applications, and see how it simplifies the process of fetching data and using it in our components.

Thank you for taking the time to read my blog post! I hope you found it informative. If you're interested in more tech-related content, be sure to follow me on ๐Ÿ”— Linkedin ๐Ÿ”— Hashnode. Thanks again for reading the blog, and I look forward to sharing more with you soon!

Did you find this article valuable?

Support webtalks by becoming a sponsor. Any amount is appreciated!

ย