Redux Redux is a open-source Javascript library for managing application stae.In this article,I will show you how to build a React Redux Hooks consume Rest API with axios.
Note: I assume that you are familiar with redux concepts. If you are new to redux,I strongly solicitation you to learn basic concept of redux.
Before we jump into the article, let me show you what we are going to create in this article.
Why to choose Redux Toolkit
- Easy way to setup store
- Support some build in dependency as like Immer js, Redux,Redux thank,Reselect,Redux devtools extension.
- No more write boilerplate
How to setup Create-React-App With Redux
For this redux tutorial lets start with setup new react application:
npx create-react-app my-app
cd my-app
npm start
Next we will add redux with:
npm install @reduxjs/toolkit react-redux
Add React Router
npm install react-router-dom
Let’s install axios with command:
npm install axios
import axios from "axios";
const API = axios.create({baseURL: process.env.REACT_APP_BASEURL});
API.interceptors.request.use((req) => {
if (localStorage.getItem("user")) {
req.headers.Authorization = `Bearer ${
JSON.parse(localStorage.getItem("user")).token
}`;
}
return req;
});
export default API
- You can change the baseURL that depends on REST APIs url that your Server configures.
Firstly configure store. Create file src/redux/store.js containing:
import { configureStore } from "@reduxjs/toolkit";
import TourReducer from "./features/tourSlice";
export default configureStore({
reducer: {
tour: TourReducer,
},
});
Then we need to connect our store to the React application. Import it into index.js like this:
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './redux/store';
import reportWebVitals from './reportWebVitals';
import './index.css';
import App from "./App";
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Create Slice Reducer and Actions
Instead of creating many folders and files for Redux (actions, reducers, types,…), with redux-toolkit we just need add one file: slice.
A slice is a collection of Redux reducer logic and actions for a single feature.Reducer are pure function which handle all logic on action type.
For creating a slice, we need:
- name to identify slice
- initial state
one or more reducer functions to define how the state can
be updatedOnce a slice is created, we can export the generated Redux action creators and the reducer function for the whole slice.
Redux Toolkit provides createSlice() function that will
auto-generate the action types and action creators for you,
based on the names of the reducer functions you provide.
Example:
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
// add your non-async reducers here
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
}
},
extraReducers: {
// extraReducers handles asynchronous requests, which is our main focus.
}
})
// Action creators
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
- Redux requires that we write all state updates immutably, by making copies of data and updating the copies. However, Redux Toolkit's createSlice and createReducer APIs use Immer inside to allow us to write "mutating" update logic that becomes correct immutable updates.
Let's create a Slice for src/redux/feature/slice
We need to use Redux Toolkit createAsyncThunk which
provides a thunk that will take care of the action types
and dispatching the right actions based on the returned
promise.Asynchronous requests created with createAsyncThunk accept
three parameters: an action type string, a callback
function (referred to as a payloadCreator), and an options
object.
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import API from "../api";
export const createTour = createAsyncThunk(
"tour/createTour",
async ({ updatedTourData, navigate, toast }, { rejectWithValue }) => {
try {
const response = await API.post("/tour", updatedTourData);
toast.success("Added Successfully");
navigate("/dashboard");
return response.data;
} catch (err) {
return rejectWithValue(err.response.data);
}
}
);
export const getToursByUser = createAsyncThunk(
"tour/getToursByUser",
async (userId, { rejectWithValue }) => {
try {
const response = await API.get(`/tour/userTours/${userId}`);;
return response.data;
} catch (err) {
return rejectWithValue(err.response.data);
}
}
);
export const updateTour = createAsyncThunk(
"tour/updateTour",
async ({ id, updatedTourData, toast, navigate }, { rejectWithValue }) => {
try {
const response = await API.patch(`/tour/${id}`, updatedTourData);
toast.success("Tour Updated Successfully");
navigate("/dashboard");
return response.data;
} catch (err) {
return rejectWithValue(err.response.data);
}
}
);
export const deleteTour = createAsyncThunk(
"tour/deleteTour",
async ({ id, toast }, { rejectWithValue }) => {
try {
const response = await API.delete(`/tour/${id}`);
toast.success("Tour Deleted Successfully");
return response.data;
} catch (err) {
return rejectWithValue(err.response.data);
}
}
);
const tourSlice = createSlice({
name: "tour",
initialState: {
tour: {},
tours: [],
userTours: [],
tagTours: [],
relatedTours: [],
currentPage: 1,
numberOfPages: null,
error: "",
loading: false,
},
reducers: {
setCurrentPage: (state, action) => {
state.currentPage = action.payload;
},
},
extraReducers: {
[createTour.pending]: (state, action) => {
state.loading = true;
},
[createTour.fulfilled]: (state, action) => {
state.loading = false;
state.tours = [action.payload];
},
[createTour.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload.message;
},
[getToursByUser.pending]: (state, action) => {
state.loading = true;
},
[getToursByUser.fulfilled]: (state, action) => {
state.loading = false;
state.userTours = action.payload;
},
[getToursByUser.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload.message;
},
[updateTour.pending]: (state, action) => {
state.loading = true;
},
[updateTour.fulfilled]: (state, action) => {
state.loading = false;
const {
arg: { id },
} = action.meta;
if (id) {
state.userTours = state.userTours.map((item) =>
item._id === id ? action.payload : item
);
state.tours = state.tours.map((item) =>
item._id === id ? action.payload : item
);
}
},
[updateTour.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload.message;
}
,
[deleteTour.pending]: (state, action) => {
state.loading = true;
},
[deleteTour.fulfilled]: (state, action) => {
state.loading = false;
const {
arg: { id },
} = action.meta;
if (id) {
state.userTours = state.userTours.filter((item) => item._id !== id);
state.tours = state.tours.filter((item) => item._id !== id);
}
},
[deleteTour.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload.message;
},
},
});
export const { setCurrentPage } = tourSlice.actions;
export default tourSlice.reducer;
tour/createTour is the action type string in this case. Whenever this function is dispatched from a component within our application, createAsyncThunk generates promise lifecycle action types using this string as a prefix:
pending: tour/createTour/pending
fulfilled: tour/createTour/fulfilled
rejected: tour/createTour/rejected
On its initial call, createAsyncThunk dispatches the tour/createTour/pending lifecycle action type. The payloadCreator then executes to return either a result or an error.
In the event of an error, tour/createTour/rejected is dispatched and createAsyncThunk should either return a rejected promise containing an Error instance, a plain descriptive message, or a resolved promise with a RejectWithValue argument as returned by the thunkAPI.rejectWithValue function (more on thunkAPI and error handling momentarily).
If our data fetch is successful, the posts/getPosts/fulfilled action type gets dispatched.
This is how I handle rest of the requests similar way.
Now we move on to the component section
By using useSelector and useDispatch from react-redux, we can read state from a Redux store and dispatch any action from a component, respectively.
Let’s set up a component to dispatch createTour when it mounts:
File AddEditTour.js:
import React, { useState, useEffect } from "react";
import { toast } from "react-toastify";
import { useNavigate, useParams } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { createTour, updateTour } from "../redux/features/tourSlice";
// import TagInput from '../components/TagInput'
import '../components/Tags.css';
const initialState = {
title: "",
description: "",
tags: [],
};
export default function AddEditTour() {
const [tourData, setTourData] = useState(initialState);
const [tagErrMsg, setTagErrMsg] = useState(null);
const { error, userTours } = useSelector((state) => ({
...state.tour,
}));
const { user } = useSelector((state) => ({ ...state.auth }));
const dispatch = useDispatch();
const navigate = useNavigate();
const { id } = useParams();
const { title, description, tags } = tourData;
useEffect(() => {
if (id) {
const singleTour = userTours.find((tour) => tour._id === id);
console.log(singleTour);
setTourData({ ...singleTour });
}
}, [id]);
useEffect(() => {
error && toast.error(error);
}, [error]);
const handleSubmit = (e) => {
e.preventDefault();
if (!tags.length) {
setTagErrMsg("Please provide some tags");
}
if (title && description && tags) {
const updatedTourData = { ...tourData, name: user?.result?.name };
if (!id) {
dispatch(createTour({ updatedTourData, navigate, toast }));
} else {
dispatch(updateTour({ id, updatedTourData, toast, navigate }));
}
handleClear();
}
};
const onInputChange = (e) => {
const { name, value } = e.target;
setTourData({ ...tourData, [name]: value });
};
const handleClear = () => {
setTourData({ title: "", description: "", tags: [] });
};
const removeTagData = deleteTag => {
setTourData({
...tourData,
tags: tourData.tags.filter((tag) => tag !== deleteTag),
});
};
const addTagData = event => {
setTagErrMsg(null);
if (event.target.value !== '') {
setTourData({ ...tourData, tags: [...tourData.tags, event.target.value] });
event.target.value = '';
}
};
const onImageChange = event => {
console.log(event.target.files[0]);
let files = event.target.files;
let reader = new FileReader();
reader.readAsDataURL(files[0]);
reader.onload = (e) => {
setTourData({ ...tourData, imageFile: e.target.result })
}
};
return (
<>
<div className="container-fluid">
<div className="form-box">
<h1>Add</h1>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">Name</label>
<input className="form-control" id="name" type="text" value={title || ""} name="title" placeholder="Name" onChange={onInputChange} />
</div>
<div className="form-group">
<label htmlFor="email">Image</label>
<input className="form-control" accept="image/*" onChange={onImageChange} type="file" />
</div>
<div className="form-group">
<label htmlFor="message">Tag</label>
<div className="tag-input">
<ul className="tags">
{tags && tags.map((tag, index) => (
<li key={index} className="tag">
<span className="tag-title">{tag}</span>
<span
className="tag-close-icon"
onClick={() => removeTagData(tag)}
>
x
</span>
</li>
))}
</ul>
<input
className="tag_input"
type="text"
onKeyUp={event => (event.key === 'Enter' ? addTagData(event) : null)}
placeholder="Press enter to add a tag"
/>
</div>
</div>
<div className="form-group">
<label htmlFor="message">Message</label>
<textarea className="form-control" id="message" value={description} name="description" placeholder="description" onChange={onInputChange} />
</div>
<input className="btn btn-primary" type="submit" defaultValue="Submit" />
</form></div>
</div>
</>
)
}
First, we define and set initial state.
Next, we create handleInputChange() function to track the values of the input and set that state for changes.
We have local state and send the POST request to the Web API. It dispatchs async Thunk createTour() with useDispatch(). This hook returns a reference to the dispatch function from the Redux store.We check dashboard component then see the difference new data added.When we updated existence data click edit button we go through the same component AddEdittour.js file now we get id and conditionally render data and finally updated data.we have deleted in the same way.
file Dashboard.js
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { deleteTour, getToursByUser } from "../redux/features/tourSlice";
import Spinner from "../components/Spinner";
import { toast } from "react-toastify";
export default function DashBoard() {
const { user } = useSelector((state) => ({ ...state.auth }));
const { userTours, loading } = useSelector((state) => ({ ...state.tour }));
const userId = user?.result?._id;
const dispatch = useDispatch();
useEffect(() => {
if (userId) {
dispatch(getToursByUser(userId));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId]);
const excerpt = (str) => {
if (str.length > 40) {
str = str.substring(0, 40) + " ...";
}
return str;
};
if (loading) {
return <Spinner />;
}
const handleDelete = (id) => {
if (window.confirm("Are you sure you want to delete this tour ?")) {
dispatch(deleteTour({ id, toast }));
}
};
return (
<>
<div className="container mt-5">
<div className="row">
<div className="col-md-12 text-center ">
<Link to={`/add`} href="#" className="card-link">Add Data</Link>
{userTours.length === 0 && (
<h3 className="text-center">No tour available with the user: {user?.result?.name}</h3>
)}
{userTours.length > 0 && (
<>
<h5 className="text-center">Dashboard: {user?.result?.name}</h5>
<hr style={{ maxWidth: "570px" }} />
</>
)}
</div>
{userTours &&
userTours.map((item,index) => (
<div className='col-md-3' key={index}>
<div className="card mb-3" >
<img src={item.imageFile} className="card-img-top img-thumbnail rounded" alt={item.title} />
<div className="card-body">
<h5 className="card-title">{item.title}</h5>
<p className="card-text"> {excerpt(item.description)}</p>
<Link to={`/edit/${item._id}`} href="#" className="card-link">Edit</Link>
<Link to="#" className="card-link" onClick={() => handleDelete(item._id)}>Delete</Link>
<Link to={`/view/${item._id}`} href="#" className="card-link">View</Link>
</div>
</div>
</div>
))}
</div>
</div>
</>
)
}
I hope you guys liked this simple Redux-toolkit crud operation.You can find complete code repository presented in this article at GitHub.
Top comments (11)
Hey! I have clone the repo but it is now working can you please provide me the backend?
Yeah sure give me your mail address I will share google drive...
julfikar Haider bro, AOA,
can you please share the project with me please..and i ll be needing your bit help as well thanks
prog.salman777@gmail.com
I have clone this repo but its not working without backend can you please shared me the backend ?
Give me your mail address I will share google drive...
Nice blog,bro.Keep shining like this
Thanks..
Julfikar Haidar can you please share backend code for this React CRUD?
Keep writing sir. It's really useful
Hey!! can you please provide me the backend code ?
my email : sarojghising13@gmail.com (to share backend code.)
Hello. Sir can you share the backend code for this ? Im learning RTK asyncThunk atm. And I think this one will help for simulation.