what is createAsyncThunk in redux ?
Tags: redux, react, tutorial, javascript
Published: 2022-07-06 10:48:00

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v8txjmjtkpk4wrika3ab.gif)


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

```javascript
// 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
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v8txjmjtkpk4wrika3ab.gif)



## provide the store to the App
wrap the enitre app with the store.

```javascript
// 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>
);
```

## create a slice

```javascript
// 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;

```

## 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.

```javascript
// src/posts/postsSlice

export const selectAllPosts = (state) => state.posts.posts
export const getPostsError = (state) => state.posts.error
export const getPostsStatus = (state) => state.posts.status
```


## add slice reducers to the store

```javascript
// 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
```

## fetch this array of posts as soon as our app loads

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

store.dispatch(fetchPosts());
```

## Posts component

```javascript
// 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
```

## TableData component

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

```javascript
// 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
```

## App component


```javascript

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

**CSS**

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

```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;
}
```

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

```javascript
"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",
```

<a href="https://www.buymeacoffee.com/ifeanyichima" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>



**Thank you, Please follow me**

[![HTML](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://linkedin.com/ifeanyi-thankgod-chima)&nbsp;[![GitHub](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/masterifeanyi)