September 27, 2023

Dispatch Actions From createAsyncThunk - React Example

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-

  1. pending
  2. fulfilled
  3. 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-

  1. 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".
  2. 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-

  1. Name of the slice is postItems.
  2. Initial state value is an empty array meaning no posts.
  3. There are two reducer functions refreshPosts and addPosts, in those functions there is no async logic (no side effect).
  4. There are two createAsyncThunk functions with names as 'postItems/fetchPostData' and 'postItems/addPost'
  5. In those createAsyncThunk functions there are callback functions to make asynchronous calls to fetch all posts and to add a post respectively.
  6. 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.
  7. 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>
  );
}
createAsyncThunk - React Example

When there is an error

dispatch action from createAsyncThunk

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

No comments:

Post a Comment