DEV Community

Cover image for what is createAsyncThunk in redux ?
Ifeanyi Chima
Ifeanyi Chima

Posted on • Updated on

what is createAsyncThunk in redux ?

Firstly, what is side effects, side effects refer to any external interaction outside an existing client application such as fetching data from an API.

In Redux, middleware has always been used to perform asynchronous tasks. Asynchronous tasks means things you have to wait for, such as fetching data from an API. A middleware is designed to enable developers to write logic that has side effects. An example is a package called redux-thunk.

Image description

Redux-thunk is used for asychronous logic (tasks).

Redux toolkit comes with built-in dependencies such as redux-thunk, because Redux toolkit includes redux-thunk by default, we can use createAsyncThunk to make asynchronous requests.

createAsyncThunk

CreateAsyncThunk is where we perform asychronous tasks in our slice. It receives two parameters

  • name of the action, the standard convention is "[slice name]/[action name]" such as "posts/fetchPosts"
  • The callback function that performs the API call and returns the result when it is finished. Our API call returns a promise (which is an object that represents the status of an asynchronous operation, in our case an API call).

For each action that is created using createAsyncThunk, there are three probable state for the promise returned. pending, fulfilled, rejected.

You decide what Redux should do in the three (3) different stages of the API call. Inside our slice we will add a property called extraReducers that holds a couple functions to handle the return of the API: pending, fulfilled and rejected.

extraReducers
You use extraReducers to handle actions that are created by createAsyncThunk. Based on the status of the promise, we will update our state.

I will assume you know a little-bit about redux toolkit, I will speed run through the set-up

Attention

please note, all files for a single feature should be in the same folder. meaning everything concerning posts should be in a folder called posts

set up a store

// src/app/store.js

import { configureStore } from '@reduxjs/toolkit'
import postsReducer from '../features/posts/postsSlice'


export const store = configureStore({
  reducer: {
   // reducer for slice goes here
  },
})

export default store
Enter fullscreen mode Exit fullscreen mode

Image description

provide the store to the App

wrap the enitre app with the store.

// index.js
import App from './App';
import { store } from './app/store'
import { Provider } from 'react-redux'

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

create a slice

// src/features/posts/postsSlice

import { createSlice } from '@reduxjs/toolkit'
import { createAsyncThunk } from '@reduxjs/toolkit'
import axios from "axios"

const BASE_URL = "https://jsonplaceholder.typicode.com/posts"

const initialState = {
    posts: [],
    status: "idle",
    error: ""
}

export const fetchPosts = createAsyncThunk("posts/fetchPosts", async () => {
    const response = await axios.get(BASE_URL)
    console.log(response.data)
    return response?.data
})

export const deletePost = createAsyncThunk("post/deletePost", async (initialPost) => {
    const {id} = initialPost
    try {
        const response = await axios.delete(`${BASE_URL}/${id}`);
        if (response?.status === 200) return initialPost;
        return `${response.status} : ${response.statusText}`;
    } catch (error) {
        return error.message
    }
})

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    // ==> normal reducer functions go here
},
  extraReducers(builder) {
        builder
            .addCase(fetchPosts.pending, (state, action) => {
                state.status = "loading"
            })
            .addCase(fetchPosts.fulfilled, (state, action) => {
                state.status = "succeeded"
                state.posts = state.posts.concat(action.payload);
            })
            .addCase(fetchPosts.rejected, (state, action) => {
                state.status = "failed"
                state.error = action.error.message
            })
            .addCase(deletePost.fulfilled, (state, action) => {
                if (!action?.payload.id) {
                    console.log("could not delete");
                    console.log(action.payload)
                    return 
                }

                const { id } = action.payload;
                const OldPosts = state.posts.filter(post => 
                post.id !== id)
                state.posts = OldPosts
            })
    }
})

export default postsSlice.reducer;

Enter fullscreen mode Exit fullscreen mode

create a bunch of selectors to access your state

The selectors make it easier so that if the nature of your state changes, you can update all of them in one place.

Attention
This is done while still inside the posts Slice.

// src/posts/postsSlice

export const selectAllPosts = (state) => state.posts.posts
export const getPostsError = (state) => state.posts.error
export const getPostsStatus = (state) => state.posts.status
Enter fullscreen mode Exit fullscreen mode

add slice reducers to the store

// src/app/store.js
import { configureStore } from '@reduxjs/toolkit'
import postsReducer from '../features/posts/postsSlice'

export const store = configureStore({
  reducer: {
    posts: postsReducer
  },
})

export default store
Enter fullscreen mode Exit fullscreen mode

fetch this array of posts as soon as our app loads

// index.js
import { fetchPosts } from './features/posts/postsSlice';

store.dispatch(fetchPosts());
Enter fullscreen mode Exit fullscreen mode

Posts component

// src/features/posts/Posts.jsx

import React from 'react'
import { useSelector } from 'react-redux/es/hooks/useSelector'
import { selectAllPosts, getPostsError, getPostsStatus } from './postsSlice'
import TableData from './TableData'

const Posts = () => {

    // selectors to access state
    const posts = useSelector(selectAllPosts);
    const status = useSelector(getPostsStatus);
    const error = useSelector(getPostsError);

    let content;



    if (status === "loading") {
        content = <div className="text-center my-5">Loading...</div>
    } else if (status === "succeeded") {
        // change the order of the posts
        const orderedPosts = posts.slice().sort((a, b) => a - b)

        content = orderedPosts.map((post, i) => (
            <TableData key={i} post={post} />
        ))
    } else if (status === "failed") {
        content = (
            <>
                <h1>Posts not found</h1>
                <p className='text-center text-danger'>{error}</p>
            </>
        )
    }



  return (
    <section className="section">
        <div className="container">
            <div className="row">
                <div className="col-12 text-center">
                    <h3>Here are all the posts</h3>
                </div>
            </div>
            <div className="row">
                <div className="col-12">
                    {content}                            
                </div>
            </div>
        </div>
    </section>
  )
}

export default Posts
Enter fullscreen mode Exit fullscreen mode

TableData component

I used seperation of concerns to make re-usable components.

// src/features/posts/TableData.jsx

import React from 'react'
import { deletePost } from './postsSlice'
import { useDispatch } from 'react-redux'
import { useNavigate } from "react-router-dom";

const TableData = ({ post }) => {

    const navigate = useNavigate();

    const { id } = post;

    const dispatch = useDispatch();

    const handleDelete = () => {
        try {
            // dispatch action to store
            dispatch(deletePost({ id })).unwrap();
            navigate("/")
        } catch (error) {
            console.log(`Failed to delete the post ${error}`)
        }
    }

    return (
        <div className="item">
            <div>
                <h3>{post.title}</h3>
                <p className="postCredit">
                    {post.body}
                </p>
            </div>
            <div>
                <button className="btn btn-danger" onClick={handleDelete}>
                    delete
                </button>
            </div>
        </div>
    ) 
}

export default TableData
Enter fullscreen mode Exit fullscreen mode

App component


import './App.css';
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"
import Posts from './features/posts/Posts';

function App() {
  return (
    <Router>
      <Routes>
         <Route path="/" element={<Posts />} />
      </Routes>
    </Router>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

CSS

Here is my CSS, you can put it in App.css or index.css

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  font-size: 1rem;
}

body{
  min-height: 100vh;
}

.App{
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  min-height: 100vh; 
}

.section{
  background-color: whitesmoke;
  border: 1px solid blue;
  flex: 1;
}

/* custom styling */

.item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  padding: 18px;
  background-color: aqua;
  border: 2px solid dodgerblue;
  margin: 10px 0;
}

.item-text{
  font-size: 1.2rem;
  margin: 0;
  padding: 0;
}
Enter fullscreen mode Exit fullscreen mode

Add the following packages to package.json and run npm install

"overrides": {
    "autoprefixer": "10.4.5"
  },
"@reduxjs/toolkit": "^1.8.3",
"bootstrap": "^5.1.3",
"react-router-dom": "^6.3.0",
"react-redux": "^8.0.2",
Enter fullscreen mode Exit fullscreen mode

Buy Me A Coffee

Thank you, Please follow me

HTML GitHub

Top comments (5)

Collapse
 
shyam3050 profile image
Shyam3050

helpfull

Collapse
 
ifeanyichima profile image
Ifeanyi Chima

Thank you, please follow me.

Collapse
 
aluwanitshishonga profile image
Tshishonga Aluwani Cedwyn

Top-notch

Collapse
 
lyutsifersaffin profile image
Xarnihtumm

Helpful and Thanks

Collapse
 
ifeanyichima profile image
Ifeanyi Chima

Thank you, please follow me.