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:
Dispatch an initial "pending" action to indicate that the asynchronous operation has started.
Perform the asynchronous operation inside the
thunk
function.If the operation is successful, dispatch a "fulfilled" action with the result of the operation.
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!