createAsyncThunk
While a Redux store possesses great state management features, it has no clue how to deal with asynchronous logic. Redux eschews handling asynchronous logic simply because it doesn’t know what you want to do with the data you fetched, let alone if it’s ever fetched — hello, errors. 🙂
Middleware has since been used in Redux applications to perform asynchronous tasks, with Redux Thunk’s middleware being the most popular package. A middleware is designed to enable developers to write logic that has side effects — which refers to any external interaction outside an existing client application, like fetching data from an API.
With Redux Toolkit, Redux Thunk is included by default, allowing createAsyncThunk
to perform delayed, asynchronous logic before sending the processed result to the reducers.
In this article, you’ll learn how to use the createAsyncThunk
API to perform asynchronous tasks in Redux apps.
You’ll need to have some knowledge about Redux to understand Redux Toolkit. However, you can reference this post to learn how to create Redux apps with Redux Toolkit.
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' const initialState = { entities: [], loading: false, } export const getPosts = createAsyncThunk( //action type string 'posts/getPosts', // callback function async (thunkAPI) => { const res = await fetch('https://jsonplaceholder.typicode.com/posts').then( (data) => data.json() ) return res }) export const postSlice = createSlice({ name: 'posts', initialState, reducers: {}, extraReducers: {}, }) export const postReducer = postSlice.reducer
The file above is a Redux slice in a React app. A slice is a function that contains your store and reducer functions used to modify store data. The createSlice
API is set to be the norm for writing Redux logic.
Within createSlice
, synchronous requests made to the store are handled in the reducers
object while extraReducers
handles asynchronous requests, which is our main focus.
Asynchronous requests created with createAsyncThunk
accept three parameters: an action type string, a callback function (referred to as a payloadCreator
), and an options object.
Taking the previous code block as a Redux store for a blog application, let’s examine getPosts
:
export const getPosts = createAsyncThunk( 'posts/getPosts', async (thunkAPI) => { const res = await fetch('https://jsonplaceholder.typicode.com/posts').then( (data) => data.json() ) return res })
posts/getPosts
is the action type string in this case. Whenever this function is dispatched from a component within our application, createAsyncThunk
generates promise lifecycle action types using this string as a prefix:
posts/getPosts/pending
posts/getPosts/fulfilled
posts/getPosts/rejected
On its initial call, createAsyncThunk
dispatches the posts/getPosts/pending
lifecycle action type. The payloadCreator
then executes to return either a result or an error.
In the event of an error, posts/getPosts/rejected
is dispatched and createAsyncThunk
should either return a rejected promise containing an Error
instance, a plain descriptive message, or a resolved promise with a RejectWithValue
argument as returned by the thunkAPI.rejectWithValue
function (more on thunkAPI
and error handling momentarily).
If our data fetch is successful, the posts/getPosts/fulfilled
action type gets dispatched.
The options
parameter is an object containing different configurations for the createAsyncThunk
API. View the list of available options.
The three lifecycle action types mentioned earlier can then be evaluated in extraReducers
, where we make our desired changes to the store. In this case, let’s populate entities
with some data and appropriately set the loading state in each action type:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' const initialState = { entities: [], loading: false, } export const getPosts = createAsyncThunk( 'posts/getPosts', async (thunkAPI) => { const res = await fetch('https://jsonplaceholder.typicode.com/posts').then( (data) => data.json() ) return res }) export const postSlice = createSlice({ name: 'posts', initialState, reducers: {}, extraReducers: { [getPosts.pending]: (state) => { state.loading = true }, [getPosts.fulfilled]: (state, { payload }) => { state.loading = false state.entities = payload }, [getPosts.rejected]: (state) => { state.loading = false }, }, }) export const postReducer = postSlice.reducer
If you’re new to Redux Toolkit, the state logic above might seem off to you. Redux Toolkit makes use of the Immer library, which allows developers to write mutable logic in reducer functions. Immer then converts your mutable logic to immutable logic under the hood.
Also, notice the function expression. For personal preference, I’ve used the map-object notation to handle the requests, mainly because this approach looks tidier.
The recommended way to handle requests is the builder callback notation because this approach has better TypeScript support (and thus IDE autocomplete even for JavaScript users).
N.B.: As your application grows, you will continue to make more asynchronous requests to your backend API and in turn handle their lifecycle action types. Consolidating all this logic into one file makes the file harder to read. I wrote an article on my approach to separating logic in your Redux Toolkit applications.
By using useSelector
and useDispatch
from react-redux, we can read state from a Redux store and dispatch any action from a component, respectively.
Let’s set up a component to dispatch getPosts
when it mounts:
import { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { getPosts } from '../redux/features/posts/postThunk' export default function Home() { const dispatch = useDispatch() const { entities, loading } = useSelector((state) => state.posts) useEffect(() => { dispatch(getPosts()) }, []) if (loading) return <p>Loading...</p> return ( <div> <h2>Blog Posts</h2> {entities.map((post) => ( <p key={post.id}>{post.title}</p> ))} </div> ) }
The Redux DevTools extension gives real-time information on the dispatch of any lifecycle action type.
It’s important to note that payloadCreator
accepts only two parameters, one of them being a custom argument that may be used in your request and the other being thunkAPI
. thunkAPI
is an object containing all of the parameters that are normally passed to a Redux Thunk function — like dispatch
and getState
. Take a look at all the acceptable parameters.
If your request requires more than one parameter, you can pass in an object when you dispatch the reducer function:
dispatch(getPosts({ category: 'politics', sortBy: 'name' })
createAsyncThunk
Remember that when your payloadCreator
returns a rejected promise, the rejected
action is dispatched (with action.payload
as undefined
). Most times, we want to display custom error messages rather than the message returned in the Error
object.
By using thunkAPI
, you can return a resolved promise to the reducer, which has action.payload
set to a custom value of your choice. thunkAPI
uses its rejectWithValue
property to perform this.
Let’s say we want to add a new post to the blog. Our createAsyncThunk
function would look something like this:
const post = { title: 'lorem', body: 'ipsum' } const addPost = createAsyncThunk( 'posts/addPost', async (post, { rejectWithValue }) => { try { const response = await fetch( 'https://jsonplaceholder.typicode.com/posts', { method: 'POST', body: JSON.stringify(post), header: { 'Content-Type': 'application/json', }, } ) const data = await response.json() return data } catch (err) { // You can choose to use the message attached to err or write a custom error return rejectWithValue('Opps there seems to be an error') } } )
Then evaluate posts/addPost/rejected
in extraReducers
:
extraReducers: { [addPost.rejected]: (state, action) => { // returns 'Opps there seems to be an error' console.log(action.payload) } }
We’ve come to a close here, devs. So far we’ve been able to go over the basic features of createAsyncThunk
and see how it works with the reducers in the slice function. The API also has some more advanced topics like canceling requests, which you can read further on.
To conclude, I’d like to mention Redux Toolkit’s RTK Query data fetching API.
RTK Query is a purpose-built, data-fetching and caching solution for Redux apps, which can eliminate the need to write any thunks or reducers to manage data fetching. So if you’ve dabbled with a library like React Query, it would be wise to use RTK Query for asynchronous logic in Redux because the syntax is quite similar.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowuseState
useState
can effectively replace ref
in many scenarios and prevent Nuxt hydration mismatches that can lead to unexpected behavior and errors.
Explore the evolution of list components in React Native, from `ScrollView`, `FlatList`, `SectionList`, to the recent `FlashList`.
Explore the benefits of building your own AI agent from scratch using Langbase, BaseUI, and Open AI, in a demo Next.js project.
Demand for faster UI development is skyrocketing. Explore how to use Shadcn and Framer AI to quickly create UI components.
7 Replies to "Using Redux Toolkit’s <code>createAsyncThunk</code>"
Thanks so much
You just saved me man
Thanks a lot
You’re welcome man. Glad I could help (:
While you correctly describe the payloadCreator as the second parameter of the async request, your use of the pseudocode “async (thunkAPI)” misleadingly may guide a reader into believing the thunkAPI is first parameter. (I certainly was fooled.) Perhaps clearer pseudocode would be “async (_, thunkAPI)”?
While redux might be a hard topic to understand, you made it easy to understand.
Good afternoon!
Excellent article, I learned a lot, but I didn’t understand how you use the getPosts function and you didn’t export it from the file, where am I going wrong?
You forgot an “export” in front of the definition of the getPosts createAsyncThunk. That’s how you can run it as a function within the dispatch() operation.
I thought I had a fundamental misunderstanding of Redux for a second there. Finally figured out it was a typo when I checked out the official docs.
https://redux-toolkit.js.org/api/createAsyncThunk
We’ve now fixed this typo — thanks for catching that, Jared!