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
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.
Can i have this code?
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……..
Thanks alot.
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