React JS: Redux Toolkit - Async Logic with Thunks
This document outlines how to handle asynchronous logic (like API calls) within your Redux application using Redux Toolkit and Thunks.
Why Thunks?
Redux actions, by default, must be synchronous. This means you can’t directly perform asynchronous operations like fetching data from an API within an action creator. Thunks solve this problem by:
- Allowing asynchronous logic:Thunks let you write action creators that return afunctioninstead of an action object. This function can then perform asynchronous operations.
- Dispatching actions:Inside the thunk function, you can dispatch standard Redux actions when the asynchronous operation completes (or fails).
- Access to
dispatchandgetState:Thunk functions receivedispatchandgetStateas arguments, giving them full access to the Redux store.
Setting up Redux Toolkit
First, ensure you have Redux Toolkit installed:
npm install @reduxjs/toolkit
Creating a Slice with Async Thunk
Let's create a slice to manage a list of posts fetched from an API.
// features/posts/postsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Define an async thunk for fetching posts
export const fetchPosts = createAsyncThunk(
'posts/fetchPosts', // Action type string
async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await response.json();
return data; // This becomes the `payload` of the fulfilled action
}
);
const postsSlice = createSlice({
name: 'posts',
initialState: {
posts: [],
loading: 'idle', // 'idle' | 'pending' | 'succeeded' | 'failed'
error: null,
},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.loading = 'pending';
state.error = null;
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.loading = 'succeeded';
state.posts = action.payload;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.error.message;
});
},
});
export const { } = postsSlice.actions; // No regular actions needed in this example
export default postsSlice.reducer;
Explanation:
createAsyncThunk: This function creates an asynchronous thunk.- The first argument is a unique action type string (e.g.,
'posts/fetchPosts'). This is used to identify the different states of the async operation. - The second argument is an
asyncfunction that performs the asynchronous operation (in this case, fetching data). The returned value from this function becomes thepayloadof thefulfilledaction.
- The first argument is a unique action type string (e.g.,
extraReducers: This allows you to define how your slice's state should change based on the different states of the async thunk.fetchPosts.pending: Handles the state when the thunk is in progress (e.g., settingloadingto 'pending').fetchPosts.fulfilled: Handles the state when the thunk successfully completes (e.g., settingpoststo the fetched data andloadingto 'succeeded').action.payloadcontains the data returned from theasyncfunction increateAsyncThunk.fetchPosts.rejected: Handles the state when the thunk fails (e.g., settingerrorto the error message andloadingto 'failed').action.errorcontains the error object.
Dispatching the Thunk
In your component, you can dispatch the thunk using dispatch:
// components/PostsList.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchPosts } from '../features/posts/postsSlice';
function PostsList() {
const dispatch = useDispatch();
const posts = useSelector((state) => state.posts.posts);
const loading = useSelector((state) => state.posts.loading);
const error = useSelector((state) => state.posts.error);
useEffect(() => {
dispatch(fetchPosts());
}, [dispatch]);
if (loading === 'pending') {
return <p>Loading posts...</p>;
}
if (error) {
return <p>Error fetching posts: {error}</p>;
}
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default PostsList;
Explanation:
useDispatch: Gets thedispatchfunction from the Redux store.useSelector: Gets theposts,loading, anderrorvalues from the Redux store.useEffect: Dispatches thefetchPoststhunk when the component mounts.- Conditional Rendering: Displays loading, error, or post list based on the
loadinganderrorstates.
Handling Multiple Async Operations
You can create multiple async thunks within a single slice or across different slices. Each thunk will have its own pending, fulfilled, and rejected action types.
Thunks with Arguments
Sometimes you need to pass arguments to your thunk. You can do this by returning a function from the action creator:
// features/posts/postsSlice.js
export const fetchPostById = createAsyncThunk(
'posts/fetchPostById',
async (postId) => { // Receive postId as argument
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
const data = await response.json();
return data;
}
);
// In your component:
dispatch(fetchPostById(1)); // Pass the postId as an argument
Best Practices
- Error Handling: Always handle errors in your
rejectedcase. Display user-friendly error messages. - Loading State: Provide visual feedback to the user while data is loading.
- Action Type Naming: Use a consistent naming convention for your action types (e.g.,
feature/actionName). - Keep Thunks Focused: Each thunk should ideally handle a single, specific asynchronous operation.
- Use
createAsyncThunk: It simplifies the handling of pending, fulfilled, and rejected states.
This provides a solid foundation for handling asynchronous logic in your Redux Toolkit applications. Remember to adapt these examples to your specific needs and API endpoints.