Sitemap

Build a CRUD app with React.js, Redux Toolkit and RTK Query

8 min readMay 23, 2024

Today you’ll build a React.js CRUD app using Redux Toolkit and RTK Query. In short, we’ll create RTK Query hooks that React will use to perform the CRUD (Create - Read - Update - Delete) operations. I will keep this very crisp and concise so that you can understand the core thing (no fancy words, dictionary is better for that 🥱)

One thing you people need to understand, that while using any kind of state management libraries, be it react-redux, redux-toolkit, zustang, etc. there are a lot of boiler plate code. So you need to either keep ready a dummy app (like today you are going to build one) or have to go for the documentations every time you want to use inside your applications. The other way round is to practice the same thing again and again so that it gets stuck inside your head. Cooooool, lets begin… 😀

Structure of the App

We are going to build the frontend (React.js) and the backend (Node.js) and will connect it to the database (MongoDB). The folder structure will look something like this :

Frontend folder structure and backend folder structure (from left to right)

To initialize the react app we will use vite and here in the Frontend section all the state management code will be present inside the redux folder. In the Backend, code is divided into models (schemas are here), routes (api endpoints) and controllers (all apis are written here).

Setting up the Project

[ Frontend ] →

Execute the below command to initialize a Vite application :

npm create vite@latest

Give your frontend directory a name, let say ‘client’, then choose React and then JavaScript. Then execute the below commands :

cd client
npm i
npm i @reduxjs/toolkit react-redux react-router-dom

[ Backend ] →

At first create a folder named ‘server’ in the root directory. Then execute the below commands :

cd server
npm init -y
npm i express dotenv mongoose cors nodemon

Also one small thing which you have to do is, go to your package.json file and write this line.

Inside the ‘scripts’, write the dev and start. Since now you have set up your initial project, lets now deep dive into the coding part…

Getting started with the Backend

Make a index.js file in the server directory. Then paste this whole code inside it.

Press enter or click to view image in full size
index.js

In the upper part, all the dependencies are being called and in the bottom part the default route is being set up and then the database and the server is listened on port 5000. Only see in line no. 14, I have imported the routes. So now lets go and create the routes file. Create a file naming productRoutes.js and write the following code :

Press enter or click to view image in full size
productRoutes.js

So you can see, all the endpoints wrt to the apis are mentioned here. So now its time to write the apis. So lets go to the controller folder and create a productController.js file. But before getting started with writing the apis we have to create the schema first, since schema will be utilized for writing the apis. So lets create another file inside the models folder named productModel.js.

Press enter or click to view image in full size
productModel.js

So this is how our schema is going to look like. But remember, here I can’t show how to create a MongoDB URI string using the cloud.mongodb.com. Just see some tutorials or blogs and create of your own. Or you can also use mongoDB Compass to locally run your DB. So now lets go to controllers.

Press enter or click to view image in full size
Press enter or click to view image in full size
productController.js

In total, 5 apis are here. And in every api, we are doing read and write operations with the DB. So our backend part is now over. The only thing which you have to do is creating a .env file and placing the mongoDB uri string key inside it.

Getting started with the Frontend

I haven’t explained the backend part much since this blog is all about redux and react. But still have shown you all the steps to create the successful backend. Lets at first jump into the boiler plate code which you people have to follow.

Frontend folder structure

If you look into the redux folder, you can see there are 2 folders. One is slicesand the other is store. Inside the slices, all the product slices and api slices will be present, so it will be a multi-file folder. But inside the store folder, we will only create a single file named store.js. Here we will configure our store to manage our state globally.

Press enter or click to view image in full size
store.js

Now we have to wrap our app with the provider

Press enter or click to view image in full size

At first, keep the reducers empty. After creating the slices, we will get back to here. Lets now create the apiSlice.js file.

Press enter or click to view image in full size
apiSlice.js

Here, the ‘SERVER_URL’ is nothing but http://locahost:5000. After this lets go back to the store and finish off the rest part.

Press enter or click to view image in full size
store.js

So now you can see, I have used apiSlice here and have enabled the ‘devTools’ to true. Its basically a chrome extension helps in visualizing the state management. Lets now create the productApiSlice.js file.

Press enter or click to view image in full size
Press enter or click to view image in full size
productApiSlice.js

So inside the apiSlice.js itself, I could have written all the hooks, but RTK gives us the injectEndpoints feature by the help of which it would automatically take the hooks from here. All the hooks are being exported by the names useGetProductsQuery, useGetProductQuery, useAddProductMutation, useUpdateProductMutation and useDeleteProductMutation. After that there are total 4 files inside the pages folder i.e, LandingPage.jsx, AddProduct.jsx, UpdateProduct.jsx and ViewProduct.jsx.

  • Below is the LandingPage.jsx code 👇:
import { useNavigate } from 'react-router-dom'
import { useDeleteProductMutation, useGetProductsQuery } from '../redux/slices/productApiSlice'
import { useEffect } from 'react'

const LandingPage = () => {
const navigate = useNavigate()
const { data: ProductInfo = [], isLoading, error, refetch } = useGetProductsQuery()
const [deleteProduct, { isLoading: isDeleting, error: deleteError }] = useDeleteProductMutation()

const handleDeleteProduct = async (id) => {
try {
await deleteProduct(id).unwrap()
} catch (err) {
console.error('Failed to delete the product:', err)
}
}

useEffect(() => {
if (refetch) refetch()
}, [refetch])
return (
<div>
<h1>Landing Page <button onClick={() => navigate("/products/add")}>Add Product</button> </h1>

{isLoading ? (<p>Loading...</p>) : error ? (console.log(error)) : (
<div style={{ width: "100%", display: "flex", flexWrap: "wrap", gap: "1rem", alignItems: "center", justifyContent: "center" }}>
{ProductInfo?.map((product) => (
<div
style={{ border: "1px solid #dedede", width: "15rem" }}
key={product.id}
>
<h3>Name : {product.name}</h3>
<p>Price : Rs. {product.price}/-</p>
<div>
<button onClick={() => navigate(`/products/${product._id}`)}>View</button>
<button onClick={() => navigate(`products/update/${product._id}`)}>Edit</button>
<button onClick={() => handleDeleteProduct(product._id)}>Delete</button>
</div>
</div>
))}
</div>

)}
</div>
)
}

export default LandingPage
  • Below is the AddProduct.jsx code 👇:
import React, { useState } from 'react'
import { useAddProductMutation } from '../redux/slices/productApiSlice'
import { useNavigate } from 'react-router-dom'

const AddProduct = () => {
const navigate = useNavigate()
const [newProduct, setNewProduct] = useState({
name: '',
price: '',
description: '',
img: ''
})

const [addProduct, { isLoading, error }] = useAddProductMutation()

const handleChange = (e) => {
const { name, value } = e.target
setNewProduct(prevState => ({
...prevState,
[name]: value
}))
}

const handleAddProduct = async () => {
try {
await addProduct(newProduct).unwrap()
navigate("/")
} catch (err) {
console.error('Failed to add the product:', err)
}
}

return (
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<h1>Add Product</h1>
<input
type="text"
name="name"
value={newProduct.name}
onChange={handleChange}
placeholder="Name"
/>
<input
type="text"
name="price"
value={newProduct.price}
onChange={handleChange}
placeholder="Price"
/>
<input
type="text"
name="description"
value={newProduct.description}
onChange={handleChange}
placeholder="Description"
/>
<input
type="text"
name="img"
value={newProduct.image}
onChange={handleChange}
placeholder="Image Url"
/>
<button onClick={handleAddProduct} disabled={isLoading}>
{isLoading ? 'Adding...' : 'Add'}
</button>
{error && <p>Error updating product: {error.message}</p>}
</div>
)
}

export default AddProduct
  • Below is the UpdateProduct.jsx code 👇:
import { useNavigate, useParams } from "react-router-dom"
import { useGetProductQuery, useUpdateProductMutation } from "../redux/slices/productApiSlice"
import { useState, useEffect } from "react"

const UpdateProduct = () => {
const navigate = useNavigate()
const { _id } = useParams()
const { data: product, isLoading, error, refetch } = useGetProductQuery(_id)
const [productInfo, setProductInfo] = useState({
name: '',
price: '',
description: '',
img: ''
})
console.log(_id)

useEffect(() => {
if (product) {
setProductInfo({
name: product.name,
price: product.price,
description: product.description,
img: product.image
})
}
}, [product])

const [updateProduct, { isLoading: isUpdating, error: updateError }] = useUpdateProductMutation()

const handleUpdateProduct = async () => {
try {
await updateProduct({ _id, product: productInfo }).unwrap()
navigate('/')
productInfo.name = ''
productInfo.price = ''
productInfo.description = ''
productInfo.img = ''

} catch (err) {
console.error('Failed to update the product:', err)
}
}

const handleChange = (e) => {
const { name, value } = e.target
setProductInfo(prev => ({
...prev,
[name]: value
}))
}

if (isLoading) return <p>Loading...</p>
if (error) return <p>Error loading product: {error.message}</p>

return (
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<h1>Update Product</h1>
<input
type="text"
name="name"
value={productInfo.name}
onChange={handleChange}
placeholder="Name"
/>
<input
type="text"
name="price"
value={productInfo.price}
onChange={handleChange}
placeholder="Price"
/>
<input
type="text"
name="description"
value={productInfo.description}
onChange={handleChange}
placeholder="Description"
/>
<input
type="text"
name="img"
value={productInfo.image}
onChange={handleChange}
placeholder="Image Url"
/>
<button onClick={handleUpdateProduct} disabled={isUpdating}>
{isUpdating ? 'Updating...' : 'Update'}
</button>
{updateError && <p>Error updating product: {updateError.message}</p>}
</div>
)
}

export default UpdateProduct
  • Below is the ViewProduct.jsx code 👇:
import { Link, useNavigate, useParams } from "react-router-dom"
import { useGetProductQuery } from "../redux/slices/productApiSlice"

const ViewProduct = () => {
const navigate = useNavigate()
const { _id } = useParams()
const { data: product = [], isLoading, error } = useGetProductQuery(_id)

return (
<div>
<h1>Landing Page</h1>

{isLoading ? (<p>Loading...</p>) : error ? (console.log(error)) : (
<div style={{ width: "100%", display: "flex", flexWrap: "wrap", gap: "1rem", alignItems: "center", justifyContent: "center" }}>
<div key={product.id}>
<h3>Name : {product.name}</h3>
<p>Price : Rs. {product.price}/-</p>
<p>Desc : {product.description}</p>
<img src={product.img} alt={product.name} />

</div>
</div>
)}
</div>
)
}

export default ViewProduct

Conclusion

I hope you have now able to create a full fledged CRUD application using MERN stack and Redux-Toolkit. Now try to make more smaller apps and once you get the idea, make a full stack application which will help you in the long run.

Thank you for staying till the end 😇

--

--

Sayan Maity
Sayan Maity

Written by Sayan Maity

Associate SDE @Listnr | Intern @ ConnectLink | Ex-Intern @ Lifense | Frontend Developer | React.js | BTech CSE'25 | https://sayan-maity.netlify.app

No responses yet