Build an app using createAsyncThunk and Redux Toolkit

Calling an API is not so straightforward while using the redux toolkit, and that’s why redux has a middleware name “createAsyncThunk()” which provided us with all the superpowers needed for handling API and response.

In this tutorial, we are going to build a complete CRUD functionality, but using mockAPI, so that you get a complete end-to-end knowledge of how to deal with API while performing Creta, Read, Update, and Delete Operations.

Pre-requisites

You must have an idea about redux beginner level, if not, learn from here

Basic knowlddge of HTML, CSS , Java Script , React and Redux

Diagram

Have a closer look on the diagram to understand the redux and asyncThunk flow

api calling flow using redux toolkit and create async thunk

Lets understand the flow –

Step 1 – An action is performed on the front-end (let’s say a button click)

Step 2 – That action is dispatched (by using useDispatch hook) to the middleware “createAsyncThunk()” written inside slice file

Step 3 – Inside createAsyncThunk() an API is made, using fetch or Axios, depending upon the method ie. GET, POST, DELETE, OR PUT

Step 4 – Now the response from the above is handled by the extraReducer , written inside createSlice method

Step 5 – And finally the state (or the global store) is updated

Step 6 – The store data is displayed back to frontend using useSelector hook

Adding redux to the project

Well, I have explained this in-depth, in my preview article, do check it out. Click here

Install Package

npm install --save react-redux @reduxjs/toolkit

src/app/store.js

import { configureStore } from "@reduxjs/toolkit";
import gitUser from "../features/gitUserSlice";

export const store = configureStore({
  reducer: {
    app: gitUser,
  },
});

Don’t forget to provide the store globally.

src/index.js

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
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>
);

Let’s create Slice now

Slice is the only file that will contain all the things needed to perform our operation

  • initialState
  • reducers
  • extraReducers

If you want a quick overview of how API call work using createAsyncthunk and extraReducers, do watch the below video

src/features/getUserSlice.js

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

//Get all user action
export const getAllUser = createAsyncThunk(
  "getUsers",
  async (args, { rejectWithValue }) => {
    try {
      const response = await fetch(
        "https://629f5d82461f8173e4e7db69.mockapi.io/Crud"
      );
      const result = await response.json();
      return result;
    } catch (err) {
      return rejectWithValue("Opps found an error", err.response.data);
    }
  }
);

//get single user
export const getSingleUser = createAsyncThunk(
  "getSingleUser",
  async (id, { rejectWithValue }) => {
    const response = await fetch(
      `https://629f5d82461f8173e4e7db69.mockapi.io/Crud/${id}`
    );

    try {
      const result = await response.json();
      return result;
    } catch (err) {
      return rejectWithValue(err.message);
    }
  }
);
//create action
export const createUser = createAsyncThunk(
  "createUser",
  async (data, { rejectWithValue }) => {
    const response = await fetch(
      "https://629f5d82461f8173e4e7db69.mockapi.io/Crud",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      }
    );
    const result = await response.json();
    return result;
  }
);

//delete single user
export const deleteUser = createAsyncThunk(
  "deleteUser",
  async (id, { rejectWithValue }) => {
    try {
      const response = await fetch(
        `https://629f5d82461f8173e4e7db69.mockapi.io/Crud/${id}`,
        {
          method: "DELETE",
        }
      );
      const result = await response.json();
      return result;
    } catch (err) {
      console.log(err);
      return rejectWithValue(err.response.data);
    }
  }
);

//update user
export const updateUser = createAsyncThunk(
  "updateUser",
  async ({ id, name, email, age, gender }, { rejectWithValue }) => {  

    try {
      const response = await fetch(
        `https://629f5d82461f8173e4e7db69.mockapi.io/Crud/${id}`,
        {
          method: "PUT",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ name, email, age, gender }),
        }
      );
      const result = await response.json();     
      return result;
    } catch (err) {
      return rejectWithValue(err);
    }
  }
);

export const gitUser = createSlice({
  name: "gitUser",
  initialState: {
    users: [],
    loading: false,
    error: null,
    searchData: [],
  },
  reducers: {
    searchUser: (state, action) => {
      state.searchData = action.payload;
    },
  },
  extraReducers: {
    [getAllUser.pending]: (state) => {
      state.loading = true;
    },
    [getAllUser.fulfilled]: (state, action) => {
      state.loading = false;
      state.singleUser = [];
      state.users = action.payload;
    },
    [getAllUser.rejected]: (state, action) => {
      state.loading = false;
      state.error = action.payload;
    },
    [createUser.fulfilled]: (state, action) => {
      state.loading = false;
      state.users.push(action.payload);
    },
    [deleteUser.pending]: (state) => {
      state.loading = true;
    },
    [deleteUser.fulfilled]: (state, action) => {
      state.loading = false;
      const { id } = action.payload;
      if (id) {
        state.users = state.users.filter((post) => post.id !== id);
      }
    },
    [deleteUser.rejected]: (state, action) => {
      state.loading = false;
      state.error = action.payload.message;
    },
    [getSingleUser.pending]: (state) => {
      state.loading = true;
    },
    [getSingleUser.fulfilled]: (state, action) => {
      state.loading = false;
      state.singleUser = [action.payload];
    },
    [getSingleUser.rejected]: (state, action) => {
      state.loading = false;
      state.error = action.payload.message;
    },
    [updateUser.pending]: (state) => {
      state.loading = true;
    },
    [updateUser.fulfilled]: (state, action) => {
      console.log("updated user fulfilled", action.payload);
      state.loading = false;
      state.users = state.users.map((ele) =>
        ele.id === action.payload.id ? action.payload : ele
      );
    },
    [updateUser.rejected]: (state, action) => {
      state.loading = false;    
      state.error = action.payload.message;
    },
  },
});

export const { searchUser } = gitUser.actions;
export default gitUser.reducer;

The above code holds all the logic of –

  • calling an API (Create, Read, Update, Delete)
  • returning an response
  • error handling
  • updating the store based on the response returned from API
  • exporting the reducer to the store

Adding Frontend Now

App.js

import { BrowserRouter, Route, Routes } from "react-router-dom";
import "./App.css";
import Create from "./components/Create";
import Read from "./components/Read";
import Edit from "./components/Edit";
import Navbar from "./components/Navbar";

function App() {
  return (
    <div className="App">
      <BrowserRouter>
        <Navbar />
        <Routes>
          <Route exact path="/" element={<Read />} />
          <Route exact path="/create" element={<Create />} />
          <Route exact path="/edit/:id" element={<Edit />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

export default App;

Navbar.js

import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { searchUser } from "../features/gitUserSlice";

const Navbar = () => {
  const [searchData, setSearchData] = useState("");
  const totalCount = useSelector((state) => state.app.users);

  const dispatch = useDispatch();

  dispatch(searchUser(searchData));

  return (
    <>
      <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <div className="container-fluid">
          <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav">
              <li class="nav-item">
                <Link to="/create" class="nav-link">
                  Create Post
                </Link>
              </li>
              <li class="nav-item">
                <Link to="/" class="nav-link">
                  All Post ({totalCount.length})
                </Link>
              </li>
            </ul>
          </div>

          <input
            class="form-control "
            type="search"
            placeholder="Search"
            value={searchData}
            onChange={(e) =>
              dispatch(searchUser(setSearchData(e.target.value)))
            }
          ></input>
        </div>
      </nav>
    </>
  );
};

export default Navbar;

Create.js

import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { createUser } from "../features/gitUserSlice";

const Create = () => {
  const [data, setData] = useState({});
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const updateData = (e) => {
    setData({
      ...data,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log("user data...", data);
    dispatch(createUser(data));
    navigate("/");
  };

  return (
    <div>
      <h2>Enter the data</h2>

      <form onSubmit={handleSubmit}>
        <div>
          <input
            type="text"
            name="name"
            placeholder="enter name"
            onChange={updateData}
          />
        </div>
        <div>
          <input
            type="email"
            name="email"
            placeholder="enter email"
            onChange={updateData}
          />
        </div>
        <div>
          <input
            type="number"
            name="age"
            placeholder="enter age"
            onChange={updateData}
          />
        </div>
        <div>
          <input
            type="radio"
            name="gender"
            // checked={updateData.gender === "Female"}
            value="Male"
            onChange={updateData}
          />
          <label>Male</label>
          <input
            type="radio"
            name="gender"
            // checked={this.state.selectedOption === "Female"}
            value="Female"
            onChange={updateData}
          />
          <label>Famale</label>
        </div>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  );
};

export default Create;

Read.js

import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getAllUser, deleteUser } from "../features/gitUserSlice";
import { Link } from "react-router-dom";
import UserModal from "./UserModal";

const Read = () => {
  const dispatch = useDispatch();
  const [show, setShow] = useState(false);
  const handleClose = () => setShow(false);
  const handleShow = () => setShow(true);
  const [radioCheck, setRadioCheck] = useState("");

  const [id, setId] = useState();

  const data = useSelector((state) => {
    return state.app;
  });

  console.log("radio...", radioCheck);

  useEffect(() => {
    dispatch(getAllUser());
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  if (data.loading) {
    return <h2>Loading...</h2>;
  }

  if (data.error != null) {
    return <h3>{data.error}</h3>;
  }

  console.log("final data to loop", data);

  return (
    <div>
      <UserModal
        handleShow={handleShow}
        handleClose={handleClose}
        show={show}
        setShow={setShow}
        id={id}
      />
      <div className="d-flex justify-content-between align-items-center mx-4">
        <h1>All Users</h1>
        <div className="d-flex gap-2">
          <div>
            <input
              class="form-check-input"
              type="radio"
              name="gender"
              checked={radioCheck === ""}
              onChange={(e) => setRadioCheck("")}
            />
            <label class="form-check-label">All</label>
          </div>
          <div class="form-check">
            <input
              class="form-check-input"
              type="radio"
              name="gender"
              value="Male"
              checked={radioCheck === "Male"}
              onChange={(e) => setRadioCheck(e.target.value)}
            />
            <label class="form-check-label">Male</label>
          </div>
          <div>
            <input
              class="form-check-input"
              type="radio"
              name="gender"
              value="Female"
              checked={radioCheck === "Female"}
              onChange={(e) => setRadioCheck(e.target.value)}
            />

            <label class="form-check-label">Female</label>
          </div>
        </div>
      </div>

      {data?.users
        .filter((item) => {
          if (data.searchData.length === 0) {
            return item;
          } else {
            return item.name
              .toLowerCase()
              .includes(data.searchData.toLowerCase());
          }
        })
        .filter((item) => {
          if (radioCheck === "") {
            return item;
          } else if (radioCheck === "Male") {
            return item.gender === radioCheck;
          } else if (radioCheck === "Female") {
            return item.gender === radioCheck;
          }
        })
        .map((ele) => (
          <div key={ele.id} className="card w-75 mx-auto my-2">
            <div className="card-body">
              <h5 className="card-title">{ele.name}</h5>
              <h6 className="card-subtitle mb-2 text-muted">{ele.email}</h6>
              <h6 className="card-subtitle mb-2 text-muted">{ele.gender}</h6>

              <button
                type="button"
                class="btn btn-primary"
                //onClick={() => setId(ele.id) && handleShow()}
                onClick={() => [setId(ele.id), handleShow(true)]}
              >
                View
              </button>

              <Link
                onClick={() => dispatch(deleteUser(ele.id))}
                className="card-link mx-2"
              >
                Delete
              </Link>
              <Link to={`/edit/${ele.id}`}>
                <span className="card-link mx-2">Edit</span>
              </Link>
            </div>
          </div>
        ))}
    </div>
  );
};

export default Read;

Edit.js

import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useParams, useNavigate } from "react-router-dom";
import { updateUser } from "../features/gitUserSlice";

const Edit = () => {
  const dispatch = useDispatch();
  const { id } = useParams();
  const navigate = useNavigate();
  const initialState = {
    name: "",
    email: "",
    age: "",
    gender: "",
  };
  const [updatedData, setUpdatedData] = useState(initialState);

  //get all data
  const { users, loading } = useSelector((state) => state.app);

  useEffect(() => {
    //retrieving single data from user list
    if (id) {
      const singleData = users.find((user) => user.id === id);
      console.log("singledata preload on edit page...", singleData);
      setUpdatedData({ ...singleData });
    }
  }, []);

  //updating state as use changes input field data
  const newData = (e) => {
    setUpdatedData({ ...updatedData, [e.target.name]: e.target.value });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log("update data..", updatedData);
    dispatch(updateUser(updatedData));
    setUpdatedData(initialState);
    navigate("/");
  };

  if (loading) {
    return <h2>Loading..</h2>;
  }

  return (
    <div>
      <h2>Update the data</h2>

      {updatedData && (
        <form onSubmit={handleSubmit}>
          <div>
            <input
              type="text"
              name="name"
              placeholder="enter name"
              value={updatedData.name}
              onChange={newData}
            />
          </div>
          <div>
            <input
              type="email"
              name="email"
              placeholder="enter email"
              value={updatedData.email}
              onChange={newData}
            />
          </div>
          <div>
            <input
              type="number"
              name="age"
              placeholder="enter age"
              value={updatedData.age}
              onChange={newData}
            />
          </div>
          <div>
            <input
              type="radio"
              name="gender"
              checked={updatedData.gender === "Male"}
              value="Male"
              onChange={newData}
            />
            <label>Male</label>
            <input
              type="radio"
              name="gender"
              checked={updatedData.gender === "Female"}
              value="Female"
              onChange={newData}
            />
            <label>Famale</label>
          </div>
          <div>
            <button type="submit">Submit</button>
          </div>
        </form>
      )}
    </div>
  );
};

export default Edit;

Conclusion

While working on any real-life project, you have to deal with API and if you are working on redux, the middleware will definitely come into the picture.

The combination of createAsyncThunk and extraReducers makes API handling easy and simple.

Frankly, it more of a process, than the logic, so once you are comfortable with the flow, it will be a piece of cake for you

I have tried to break it down and explain you as simply as I can. Don’t forget to watch my tutorial on the same.

Thankyou for reading. Happy Learning.

Help Others

4 Comments

  1. Youtube have many videos on redux toolkit… after a week i got ur channel and just want to say it is far better than premium course… thank u keep it up and help those who cant afford a premium course……… thank u thank u……..

  2. Himanshu Routsays:

    Sir I am not able to get the value of state.users = action.payload

    [showUser.fulfilled]: (state, action) => {
    state.loading = false;
    state.users = action.payload;
    console.log(“stateee—->”,state.users);
    },
    in userDetailsSlice.js

Leave a Reply

Your email address will not be published. Required fields are marked *