In the post createAsyncThunk in Redux + React Example we have seen how to use createAsyncThunk to write side effects when using Redux. In this post we'll see how to dispatch an action from the callback function of createAsyncThunk.
How createAsyncThunk works
createAsyncThunk will generate three Redux action creators for three states your asynchronous call may be in-
- pending
- fulfilled
- rejected
createAsyncThunk function abstracts the approach for handling async request lifecycles which is either resolved or rejected.
- Dispatches the fulfilled action with the promise value as action.payload when the promise resolved successfully.
- If the promise resolved with an error, dispatches the rejected action.
In your state slice you use extraReducers to handle dispatched actions that are created by createAsyncThunk. Based on the action that is dispatched (pending, fulfilled, rejected) you can update the state stored in the store.
Dispatch from createAsyncThunk
In your application, rather than handling the state in these action creators, you may want to dispatch action to the same slice or to another slice. That's what we'll see in this post how to dispatch an action inside the callback function of createAsyncThunk.
createAsyncThunk function accepts three parameters-
- A Redux action type string which signifies the name of the action. Convention is to use "slice name/action name" as the action name for example "posts/fetchPostData".
- A callback function where asynchronous logic is written and the function returns a promise containing the result
of asynchronous logic.
The callback function will be called with two arguments:
- arg: A single value, containing the first parameter that was passed to the thunk action creator when it was dispatched. For example, you can pass an ID to fetch data based on that ID, data that has to be saved.
- thunkAPI: an object containing all of the parameters that are normally passed to a Redux thunk function.
Some of the main ones are-
- dispatch: the Redux store dispatch method.
- getState: the Redux store getState method.
- rejectWithValue(value, [meta]): rejectWithValue is a utility function that you can return (or throw) in your action creator to return a rejected response with a defined payload and meta.
- fulfillWithValue(value, meta): fulfillWithValue is a utility function that you can return in your action creator to fulfill with a value while having the ability of adding to fulfilledAction.meta.
As you can see callback function takes two arguments one of them is thunkAPI which is an object having one of the properties as dispatch. This Redux store dispatch() method can be used to dispatch actions.
For example-
createAsyncThunk( 'postItems/addPost', async(post, {dispatch}) => { try{ //async logic.. ... // dispatch success notification action dispatch(notificationActions.showNotification({ status: 'Success', message: 'Post data successfully inserted, title is: ' + data.title }) ); } catch(error){ // dispatch error notification action dispatch(notificationActions.showNotification({ status: 'Error', message: 'Error while inserting post ' + error.message })); } } );
createAsyncThunk React example
This redux-thunk example uses jsonplaceholder API to fetch posts and add new post. These async fetch requests are done in createAsyncThunk functions.
1. Create state slices
We'll have two state slices one for storing post state and another for notification state.
src\slice\post-slice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { notificationActions } from "./notification-slice"; const postSlice = createSlice({ name: 'postItems', initialState: { posts: [] }, reducers: { refreshPosts: (state, action) => { state.posts = action.payload }, addPost: (state, action) => { const newPost = action.payload; state.posts.push({ id: newPost.id, title: newPost.title, body: newPost.body, userId: newPost.userId }) } }, }); export const fetchPostData = createAsyncThunk( 'postItems/fetchPostData', async (_, {dispatch}) => { try{ const response = await fetch('https://jsonplaceholder.typicode.com/posts'); if(!response.ok){ throw new Error('Fetching posts failed..'); } const data = await response.json(); dispatch(postActions.refreshPosts(data)); } catch(error){ console.log("Error in fetching " + error.message); dispatch(notificationActions.showNotification({ status: 'Error', message: 'Error while fetching posts ' + error.message })); } } ); export const addPostData = createAsyncThunk( 'postItems/addPost', async(post, {dispatch}) => { try{ const response = await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(post) }); if(!response.ok){ throw new Error('Inserting new posts failed.'); } const data = await response.json(); dispatch(notificationActions.showNotification({ status: 'Success', message: 'Post data successfully inserted, title is: ' + data.title }) ); } catch(error){ dispatch(notificationActions.showNotification({ status: 'Error', message: 'Error while inserting post ' + error.message })); } } ); export const postActions = postSlice.actions; export default postSlice.reducer;
Some of the important points about this state slice are-
- Name of the slice is postItems.
- Initial state value is an empty array meaning no posts.
- There are two reducer functions refreshPosts and addPosts, in those functions there is no async logic (no side effect).
- There are two createAsyncThunk functions with names as 'postItems/fetchPostData' and 'postItems/addPost'
- In those createAsyncThunk functions there are callback functions to make asynchronous calls to fetch all posts and to add a post respectively.
- If call to fetch posts is successful then an action is dispatched from the callback function to update the posts state. In case of error notificationAction is dispatched. Same way with the call to add new post. A notification action is dispatched with success or error message in case of success or error.
- If first argument is not passed to the callback function only second argument then it has to be in the
following format.
async (_, {dispatch}) => { }
src\slice\notification-slice.js
import { createSlice } from "@reduxjs/toolkit"; const notificationSlice = createSlice({ name: 'notification', initialState: {notification: null}, reducers: { showNotification: (state, action) => { state.notification = { status: action.payload.status, message: action.payload.message }; } } }) export const notificationActions = notificationSlice.actions; export default notificationSlice.reducer;
2. Setting up a store with Reducers
src\store\postStore.js
import { configureStore } from "@reduxjs/toolkit"; import postReducer from "../slice/post-slice"; import notificationReducer from "../slice/notification-slice"; const store = configureStore({ reducer: { post: postReducer, notification: notificationReducer } });
3. Provide store to React
React Redux includes a <Provider /> component, which makes the Redux store available to the React app. We'll wrap <App /> component with the Provider which guarantees that the store provided with this Provider is available to App component and all its child components. That change can be done in index.js file.
src\index.js
import { Provider } from 'react-redux'; import store from './store/postStore'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <Provider store={store}><App /></Provider> </React.StrictMode> );
4. Components
For fetching posts and displaying them there are two components Posts.js and PostItem.js.
For showing notification there is Notification.js.
src\components\Notification\Notification.js
const Notification = (props) => { return ( <div className="alert alert-primary" role="alert"> <p>{props.status} : {props.message}</p> </div> ); }; export default Notification;
Note that Bootstrap classes are used to show alert notification.
src\components\Post\Posts.js
This component dispatches thunk function in useEffect() hook and then loops over the fetched posts to display them using PostItem component.
import { Fragment, useEffect } from "react"; import { useDispatch, useSelector } from "react-redux" import { fetchPostData } from "../../slice/post-slice"; import Notification from "../Notification/Notification"; import PostItem from "./PostItem"; const Posts = () => { const posts = useSelector(state => state.post.posts); const notification = useSelector(state => state.notification.notification); const dispatch = useDispatch(); useEffect(() => { dispatch(fetchPostData()); }, [dispatch]) return ( <Fragment> { notification && <Notification status={notification.status} title={notification.title} message={notification.message}/> } <h2>Posts</h2> <ul> { posts.map(post=> ( <PostItem key={post.id} post={post}></PostItem> ) )} </ul> </Fragment> ); } export default Posts;
src\components\Post\PostItem.js
const PostItem = (props) => { return ( <li> <span>User: {props.post.userId}</span> <h2>{props.post.title}</h2> <span>{props.post.body}</span> </li> ); } export default PostItem;
I can test Posts component by adding this element to App.js
function App() { return ( <div className="App"> <Posts /> </div> ); }
When there is an error
Adding post
For adding new post following component is used.
src\components\Post\AddPost.js
import { Fragment } from "react"; import { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { addPostData } from "../../slice/post-slice"; import Notification from "../Notification/Notification"; const AddPost = () => { const notification = useSelector(state => state.notification.notification); const dispatch = useDispatch(); const [formField, setState] = useState({ title: '', body: '', userId: '' }); const handleInputChange = (event) => { const target = event.target; const value = target.value; const name = target.name; setState((prevState) => { return {...prevState, [name]: value}; }); } const formSubmitHandler = (event) => { event.preventDefault(); dispatch(addPostData({ title: formField.title, body: formField.body, userId: formField.userId })); } return( <Fragment> { notification && <Notification status={notification.status} title={notification.title} message={notification.message}/> } <form onSubmit={formSubmitHandler}> Title: <input type="text" placeholder="Enter Title" name="title" onChange={handleInputChange}></input> Body: <input type="text" placeholder="Enter Post Content" name="body" onChange={handleInputChange}></input> User ID: <input type="number" placeholder="Enter User ID" name="userId" onChange={handleInputChange}></input> <button type="submit" onClick={formSubmitHandler}>Add Post</button> </form> </Fragment> ); } export default AddPost;
That's all for the topic Dispatch Actions From createAsyncThunk - React Example. If something is missing or you have something to share about the topic please write a comment.
You may also like
- Redux Toolkit in React With Examples
- Redux Thunk in React With Examples
- React useImperativeHandle Hook With Examples
- How to Loop in React
- forwardRef in React With Examples
- Java Stream Collectors.collectingAndThen() Examples
- JDBC Database Connection Steps
- Encapsulation in Java – OOPS Concepts
- Spring IoC Container Types – ApplicationContext and BeanFactory
- Spring Boot + Spring Data REST Example
No comments:
Post a Comment