Using Redux Toolkit's createAsyncThunk to Handle Async API Calls
Learn how to replace manual async action boilerplate with createAsyncThunk, which automatically generates pending, fulfilled, and rejected actions for predictable state updates.
30 May 2026, 19:58 UTC

Problem: Boilerplate for Async Actions in Redux
When fetching data from an API with plain Redux, you must define three action types (request, success, failure), create action creators, and write reducer logic for each stage. This repetitive code increases the chance of mistakes and obscures the core logic.
Takeaway
Redux Toolkit’s createAsyncThunk generates the pending, fulfilled, and rejected action types and creators automatically. By integrating the thunk with a slice’s extraReducers, you get a predictable async flow with minimal boilerplate.
Prerequisites
- Node.js project with npm or yarn.
- Basic Redux store already configured (store, provider, root reducer).
- Redux Toolkit version 1.5.0 or higher installed (
npm install @reduxjs/toolkit). - Familiarity with React components (optional, for dispatching).
Procedure
-
Install Redux Toolkit (if not already)
Run in your project root:
npm install @reduxjs/toolkitVerify installation by checking
package.jsonfor"@reduxjs/toolkit": "^1.5.0"or higher. -
Create an async thunk
Define a function that returns a promise (e.g., a fetch call). Use
createAsyncThunkto wrap it.import { createAsyncThunk } from '@reduxjs/toolkit' // Placeholder for your API endpoint const API_URL = 'https://example.com/api/posts' export const fetchPosts = createAsyncThunk( 'posts/fetchPosts', async (_, { rejectWithValue }) => { try { const response = await fetch(API_URL) if (!response.ok) { throw new Error('Network response was not ok') } return await response.json() } catch (err) { return rejectWithValue(err.message) } } )The thunk will automatically produce three action types:
posts/fetchPosts/pending,posts/fetchPosts/fulfilled, andposts/fetchPosts/rejected. -
Create a slice with extraReducers
In your slice file, handle the generated actions to update state.
import { createSlice } from '@reduxjs/toolkit' import { fetchPosts } from './postsThunk' const postsSlice = createSlice({ name: 'posts', initialState: { items: [], loading: false, error: null }, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchPosts.pending, (state) => { state.loading = true state.error = null }) .addCase(fetchPosts.fulfilled, (state, action) => { state.loading = false state.items = action.payload }) .addCase(fetchPosts.rejected, (state, action) => { state.loading = false state.error = action.payload || action.error.message }) } }) export default postsSlice.reducer -
Add the slice reducer to the store
Ensure the store includes the reducer.
import { configureStore } from '@reduxjs/toolkit' import postsReducer from './postsSlice' const store = configureStore({ reducer: { posts: postsReducer } }) export default store -
Dispatch the thunk from a component
In a React component, dispatch
fetchPosts(e.g., in a useEffect).import { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { fetchPosts } from './postsThunk' function PostsList() { const dispatch = useDispatch() const { items, loading, error } = useSelector(state => state.posts) useEffect(() => { dispatch(fetchPosts()) }, [dispatch]) if (loading) returnLoading…
if (error) returnError: {error}
return (-
{items.map(post => (
- {post.title} ))}
-
Expected checks
Open Redux DevTools and dispatch the thunk. You should observe three actions in order:
posts/fetchPosts/pending– setsloadingto true.- Either
posts/fetchPosts/fulfilledwith payload data – updatesitemsand clears loading, orposts/fetchPosts/rejectedwith error message – setserrorand clears loading.
Verify that the UI reflects the loading state, then either the list of posts or an error message.
-
Recovery options
If the thunk rejects, the
rejectedcase already stores an error message in state. You can:- Show a retry button that re‑dispatches
fetchPosts. - Reset the error by dispatching a custom action (e.g.,
clearError) defined in the slice’sreducers. - Optionally roll back optimistic updates by resetting
itemsto previous values in therejectedcase.
- Show a retry button that re‑dispatches
Limitations
While createAsyncThunk eliminates boilerplate, you still need to:
- Define proper TypeScript types for the thunk’s argument and return value if using TypeScript.
- Handle cancellation manually if your use case requires aborting ongoing requests (e.g., with
AbortController). - Remember that the thunk only automates the three lifecycle actions; additional side‑effects (like logging) must be added inside the payload creator or via middleware.
Verification
To confirm everything works:
- Run the application and open Redux DevTools.
- Trigger the component that dispatches
fetchPosts. - Check that the three expected actions appear in the log.
- Inspect the state changes:
loadingtoggles,itemsfills with data orerroris set. - If an error occurs, verify that the UI shows the error message and that a retry button re‑dispatches the thunk correctly.
No state‑changing operation is performed beyond the normal Redux update flow, so a formal rollback is not required; you can simply re‑dispatch the thunk or clear error state as described.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.