September 25, 2023

createAsyncThunk in Redux + React Example

In the post Redux Thunk in React With Examples we saw a way to write asynchronous logic while using Redux. One of the rules of the reducers is that reducers must not do any asynchronous logic or any other side effects. Redux-thunk is the most common async middleware which lets you write plain functions that may contain async logic directly. Another way to write asynchronous logic with Redux is to use createAsyncThunk() function.

createAsyncThunk in redux-thunk

By using createAsyncThunk() function, it becomes easy to use Promise or async/await (which also returns a Promise). This function abstracts the approach for handling async request lifecycles which is either resolved or rejected.

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.
  3. Options- An object with the optional fields. Some of the fields are as given below.
    • condition(arg, { getState, extra } ): boolean | Promise<boolean>: a callback that can be used to skip execution of the payload creator and all action dispatches, if desired. See Canceling Before Execution for a complete description.
    • dispatchConditionRejection: if condition() returns false, the default behavior is that no actions will be dispatched at all. If you still want a "rejected" action to be dispatched when the thunk was canceled, set this flag to true.

Return value of createAsyncThunk

createAsyncThunk will generate three Redux action creators for three states your asynchronous call may be in-

  1. pending
  2. fulfilled
  3. rejected

Each of these action creators will be attached to the thunk action creator which is returned by the createAsyncThunk.

For example, if we take the same "fetchPostData" example as mentioned above following functions will be generated by createAsyncThunk.

  • fetchPostData.pending, an action creator that dispatches 'posts/fetchPostData/pending' action
  • fetchPostData.fulfilled, an action creator that dispatches 'posts/fetchPostData/fulfilled' action
  • fetchPostData.rejected, an action creator that dispatches 'posts/fetchPostData/rejected' action

createAsyncThunk flow

When createAsyncThunk is dispatched the thunk will-

  1. Initially dispatch the pending action.
  2. Call the callback function where we have asynchronous logic and wait for the returned promise to settle.
  3. When the promise settles:
    • If the promise resolved successfully, dispatch the fulfilled action with the promise value as action.payload
    • if the promise resolved with a rejectWithValue(value) return value, dispatch the rejected action with the value passed into action.payload and 'Rejected' as action.error.message.
    • if the promise failed and was not handled with rejectWithValue, dispatch the rejected action with a serialized version of the error value as action.error
  4. Return a fulfilled promise containing the final dispatched action (either the fulfilled or rejected action object).

extraReducers

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 will update the stored state.

You can reference the action creators in extraReducers using either the object key notation or the "builder callback" notation.

Using object key notation-

createSlice({
    name: 'postItems',
    initialState: {},
    reducers: {},
    extraReducers: {
       [fetchPostData.pending]: (state, action) => {},
       [fetchPostData.fulfilled]: (state, action) => {},
       [fetchPostData.rejected]: (state, action) => {}
    }
})

Using builder callback notation-

createSlice({
    name: 'postItems',
    initialState: {},
    reducers: {},
    extraReducers: (builder) => {
        builder
        .addCase(fetchPostData.pending, (state, action) => {})
        .addCase(fetchPostData.fulfilled, (state, action) => {})
        .addCase(fetchPostData.rejected, (state, action) => {})
    }
})

createAsyncThunk React example

This redux-thunk example with createAsyncThunk uses jsonplaceholder API to fetch posts and add new post.

1.Create state slice

We have one state slice for storing post state.

src\slice\post-slice.js

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";

const postSlice = createSlice({
    name: 'postItems',
    initialState: {
        posts: [],
        status: "idle", 
        error: null
    },
    reducers: {},
    extraReducers: (builder) => {
        builder
        .addCase(fetchPostData.pending, (state, action) => {
            state.status = "Loading";
        })
        .addCase(fetchPostData.fulfilled, (state, action) => {
            state.status = "Success";
            state.posts = action.payload;
        })
        .addCase(fetchPostData.rejected, (state, action) => {
            state.status = "Error";
            state.error = action.payload;
        })
        .addCase(addPostData.fulfilled, (state, action) => {
            state.status = "Success";
            const newPost = action.payload;
            state.posts.push({
                id: newPost.id,
                title: newPost.title,
                body: newPost.body,
                userId: newPost.userId
            })
        })
        .addCase(addPostData.rejected, (state, action) => {
            state.status = "Error";
            state.error = action.payload;
        })
    }

});

export const fetchPostData = createAsyncThunk(
    'postItems/fetchPostData',
    async (_, {rejectWithValue}) => {            
        try{
            const response = await fetch('https://jsonplaceholder.typicode.com/posts');
            if(!response.ok){
                throw new Error('Fetching posts failed..');
            }
            const data = await response.json();
            return data;    
        } catch(error){
            console.log("Error in fetching " + error.message);
            return rejectWithValue(error.message);
            //return error.message;

        }
    }
);

export const addPostData = createAsyncThunk(
    'postItems/addPost',
    async(post, {rejectWithValue}) => {
        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();
            return data;
        } catch(error){
            return rejectWithValue(error.message);         
        }
    }
);

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 contains an empty array meaning no posts, state as "idle" and error as null.
  3. There are two createAsyncThunk functions with names as 'postItems/fetchPostData' and 'postItems/addPost'
  4. In those createAsyncThunk functions there are callback functions to make asynchronous calls to fetch all posts and to add a post respectively.
  5. In case of error, rejectWithValue utility function is returned.
  6. If second argument to the callback function is rejectWithValue and there is no first argument then it has to be in the following format.
    async (_, {rejectWithValue}) => { }
    

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,
    }
});

export default store;

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>
);

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;

Here Bootstrap classes are used to show alert notification.

src\components\Post\Posts.js

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 status = useSelector(state => state.post.status);
    const error = useSelector(state => state.post.error);
    let uiContent;    
    const dispatch = useDispatch();

    useEffect(() => {
        dispatch(fetchPostData());
    }, [dispatch])
    
    if(status === "Success"){
        uiContent = 

            <ul>{posts.map(post => (
                    <PostItem key={post.id} post={post}></PostItem>
                ))}
            </ul>;            

    }else if(status === "Error"){
        uiContent = <Notification status={status} message={error}/>
    }else if(status === "Loading"){
        uiContent = <Notification status={status} message="Loading..."/>
    }
    return (
        <Fragment>
            <h2>Posts</h2>
            {uiContent}
       </Fragment>
    );
}
export default Posts;

This component dispatches function in useEffect() hook, based on the updated status it then loops over the fetched posts to display them using PostItem component or show the appropriate notification message.

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

When there is an error

createAsyncThunk in Redux

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 status = useSelector(state => state.post.status);
    const error = useSelector(state => state.post.error);
    let uiContent;
    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
        }));
    }
    if(status === "Success"){
        uiContent = <Notification status={status} message="Post successfully inserted"/>


    }else if(status === "Error"){
        uiContent = <Notification status={status} message={error}/>
    }
    return(
        <Fragment>
            { uiContent }
            <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;

Points to observe in the above code are-

  1. Uses useState() hook to get form fields state.
  2. At the click of "Add Post" button, formSubmitHandler() function is called where thunk function to add post is dispatched.

Source- https://redux-toolkit.js.org/api/createAsyncThunk

That's all for the topic createAsyncThunk in Redux + 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