Mastering Async Thunks in Redux Toolkit: Effortlessly Handle Asynchronous Actions in React

Mastering Async Thunks in Redux Toolkit: Effortlessly Handle Asynchronous Actions in React

hey, everyone in this blog post I want to write about Async Thunk what is it, how to use it, basically everything about it. So if you are interested in how to use redux with API's then this is the perfect guide for it. Before starting to read this guide let me warn you that it is not for absolute beginners in redux, you need to know how how to setup store and how to create a slice and how to update states in the store using the actions that were created in the slice. I have written articles on those particular topics for absolute beginners in redux make sure to visit that here are the links, on how to use redux: click here, after you read this you can build a project using that knowledge and link to that article with source code is here.

What is Async Thunk

let's start by understanding what thunks are, the basic or absolute working principle of a thunk is it wraps a function or expression to delay computation. Thunk can also be used to add additional calculations to a function. Thunks provide lazy evaluation and are widely used in functional programming languages.

Why do we need it?

well redux is all about synchronous functions, but there will be situations where you need to use asynchronous functions you can use then with async thunk. Async thunk enables us to use asynchronous functions inside the redux toolkit, Thunks are functions that are dispatched as actions and can have side effects, such as making API calls, before dispatching other actions. Thunks receive that dispatch and getState functions as arguments, allowing them to dispatch actions and access the current state of Redux store.

How to use Async Thunk?

An async thunk is usually implemented using a middleware like Redux Thunk. Async thunks are implemented using a middleware Redux Thunk. Async thunks are action creators that return a function instead of a plain action object. This returned function can perform asynchronous tasks such as making API calls, and dispatch multiple actions based on the results.

In latest version, we use the Redux toolkit and we use createAsyncThunk for implementing asynchronous logic in our application. Let's see at a basic example,

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

// First, create the thunk
const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId, thunkAPI) => {
    const response = await userAPI.fetchById(userId)
    return response.data
  }
)

const initialState = {
  entities: [],
  loading: 'idle',
} as UsersState

// Then, handle actions in your reducers:
const usersSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    // standard reducer logic, with auto-generated action types per reducer
  },
  extraReducers: (builder) => {
    // Add reducers for additional action types here, and handle loading state as needed
    builder.addCase(fetchUserById.fulfilled, (state, action) => {
      // Add user to the state array
      state.entities.push(action.payload)
    })
  },
})

// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))

This is a direct example that was taken from redux toolkit documentation, let's go through this case and see how createAsyncThunk has been implemented.

createAsyncThunk takes in three parameters:

  1. Type which is String.

  2. a payload creator callback and

  3. options

  4. Type: In the above example the type is 'users/fetchByIdStatus', this will create the following action types such as:

    1. 'users/fetchByIdStatus/pending'

      1. 'users/fetchByIdStatus/fulfilled and

      2. 'users/fetchByIdStatus/rejected'

PayLoad creator Callback: This generally a call back function that returns a promise that contains a result of synchronous code. If the promise is rejected then it should return the reject error. The payload creator can contain whatever logic such as fetch calls.

And after the create action is done we will go to the related slice and implement actions in extraReducers based on the createAsyncThunk ouput, since it outputs a promise we generate cases based on weather the promise is pending, fulfilled or rejected, based on this we modify the store states.

And after finally the we use the dispatch function to use the thunk outside the slice, remember in order to use the thunk outside we need to export it.

Real Example:

In one of my previous we I have created a notice board where authors can post notice on the board, but in that project we haven't used any API,, but if you have a backen then you would like to use createAsyncThunk for now lets use that project, if haven't used that project you can ge the starter project up in this github repo, make sure you are in the main branch if you want to code yourself after reading this post or else you can change the branch to AsyncThunk branch for updated version of the project.

Now lets implement async functions using createAsyncThunk, first let's implement it in the posts slice:

Go to the features folder and then to the posts folder, now open PostsSlice.jsx

Now modify the code as instucted below:

import { createSlice, nanoid, createAsyncThunk } from "@reduxjs/toolkit";
import {sub} from 'date-fns';
import axios from "axios";

const POSTS_URL = 'https://jsonplaceholder.typicode.com/posts';

const initialState =  {
    posts: [],
    status: 'idle',   // 'idle' | 'loading' | 'succeeded'| 'failed'
    error: null 
}

export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
    try{
        const response = await axios.get(POSTS_URL)

        return [...response.data];
    }catch(error){
        return error.message;
    }
})

export const addNewPost = createAsyncThunk('posts/addNewPost', async(initalPost) => {
    try{
        const response = await axios.post(POSTS_URL, initalPost)
        return response.data
    }catch(err){
        return err.message
    }
})

const postsSlice = createSlice({
    name: "posts",
    initialState,
    reducers:{
        postAdded: {
                reducer(state,action){
                state.posts.push(action.payload)
        },
        prepare(title,content,userId){
            return{
                payload: {
                    id:nanoid(),
                title,
                content,
                date: new Date().toISOString(),
                userId,
                reactions: {
                    thumbsUp: 0,
                    wow: 0,
                    heart: 0,
                    rocket: 0,
                }
                }
            }
        }
    },
    reactionAdded(state,action){
        const {postId, reaction} = action.payload

        const existingPost = state.posts.find(post => post.id === postId)

        if(existingPost){
            existingPost.reactions[reaction]++
        }
    }
    }, // here extra reducers take a bulider as parameter through wich we can create a reducer for every specific action type
    extraReducers(builder){
        builder
        .addCase(fetchPosts.pending, (state,action) => {
            state.status = 'loading';
        })
        .addCase(fetchPosts.fulfilled, (state,action) => {
            state.status = 'succeeded';
            // adding date and reactions because our fake api posts call doesn't have a date and reactions

            let min =1;
            const loadedPosts = action.payload.map((post) => {
                post.date = sub(new Date(), { minutes: min++ }).toISOString();
                post.reactions = {
                    thumbsUp: 0,
                    wow: 0,
                    heart: 0,
                    rocket: 0,
                    coffee: 0
                }
                return post;
            });

            // Add any fetch post to the array

            state.posts = state.posts.concat(loadedPosts)
        })
        .addCase(fetchPosts.rejected, (state,action) => {
            state.status = 'failed'
            state.error = action.error.message
        })
        .addCase(addNewPost.fulfilled, (state,action)=> {
            action.payload.userId = Number(action.payload.userId)
            action.payload.date = new Date().toISOString();
            action.payload.reactions = {
                thumbsUp: 0,
                    wow: 0,
                    heart: 0,
                    rocket: 0,
                    coffee: 0
            }
            console.log(action.payload);
            state.posts.push(action.payload)
        })
    }
})

export const selectAllPosts = (state) => state.posts.posts;
export const getPostStatus = (state) => state.posts.status;
export const getPostError = (state) => state.posts.error;

export const {postAdded, reactionAdded} = postsSlice.actions

export default postsSlice.reducer

If you compare to the original project we have made few changes to the intial state which will now be an object with posts as an empty array, status and an error, we have also setup a API URL which will give us the posts, this is a fake API URL from the internet.

Now we have created an createAsyncThunk with type 'posts/fetchPosts' and a async callback function inside which we have used axios to get the posts and we will return data or an error based upon the fetch success or failure, all this createAsyncThunk is stored in a vaiable called ' fetchPosts ' and we have exported it. Now based upon that we have created actions under the extraReducers in the postsSlice where we have implemented state updates based upon the fetchPosts pending, fulfilled or rejected as we discussed above.

Then inside addNewPost constant, we have implemented createAsyncThunk where we will take post as an argument and in the callback function we have used axios.post and passed the post as content for the the API, and as usual we have created actions in the extraReducers under postsSlice.

Now implement a similar in the UsersSlice.jsx

Modify the code according to the below:

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

const USERS_URL = 'https://jsonplaceholder.typicode.com/users';

const initialState = []

export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
    try{
        const response = await axios.get(USERS_URL)
        return [...response.data]
    }catch(error){
        return error.message
    }
})

const UsersSlice = createSlice({
    name: 'users',
    initialState,
    reducers:{},
    extraReducers(builder){
        builder.addCase(fetchUsers.fulfilled, (state,action) => {
            return action.payload  //we are using this approach because we here we are returning an entire state instead of updating so that users will not be added twice
        })
    }
})

export const selectAllUsers = (state) => state.users;

export default UsersSlice.reducer;

Now to use this dipatch we hve used the useDispatch function inside the postsLists.jsx as

dispatch((fetchPosts())

and inside the AddPostForm we use it as

dispatch(addNewPost({title, body:content, userId})).unwrap()

over here we use the unwrap() function as this will throw an fulfilled action or throw a rejected error based on the promise that will be returned by the creatAsyncThunk.

Here i haven't explained the entire process of the poject because the goal of this post is to make you understand the createAsyncThunk in redux toolkit and its implementation in the pojects like one above. Go through the project from the above repository link.

If you like this article please leave a like if you want more posts like this you can subscibe to my newsletter and if you want to support me you can do that by heading to the support tab in the navbar or you can use this link directly.

Did you find this article valuable?

Support Saravan Krishna's blog by becoming a sponsor. Any amount is appreciated!