Download as pdf or txt
Download as pdf or txt
You are on page 1of 28

Search Write

Be part of a better internet. Get 20% off membership for a limited time

Creating an E-commerce Site with


MERN Stack — Part VI
Tókos Bence · Follow
10 min read · Jan 15, 2024

11
React reducer (Simplifying State Management in React)

In modern web development, building complex user interfaces requires an


effective state management solution. React, a popular JavaScript library,
offers various approaches for managing state, one of which is through the
use of reducers. In React, state refers to the data that determines how
components behave and render. Traditionally, state management was often
accomplished by lifting state up to a common parent component (like we did
in the card component) or using libraries like Redux.

React Reducer is a built-in feature of React that allows for a more structured
and centralized approach to managing state within components. It is based
on the concept of the reducer function, inspired by the Redux library, but
designed specifically for React. A reducer function takes in the current state
and an action, and returns a new state based on that action.

The Basic Structure of a Reducer

A reducer is a pure function that receives two arguments: the current state
and an action object. The state represents the current state of the
application, while the action describes the type of state change to be
performed. Inside the reducer, we use a switch statement to determine the
appropriate action type and update the state accordingly.

Defining Actions and Action Types

Actions are plain JavaScript objects that describe an intention to change the
state. They typically consist of a type property, which indicates the action
type, and optionally, a payload property that carries additional data. Action
types are typically defined as constants to avoid typographical errors.

Using the useReducer Hook

React Reducer is typically used in conjunction with the useReducer hook,


another built-in feature of React. The useReducer hook takes a reducer
function and an initial state as arguments and returns the current state and a
dispatch function. The dispatch function is used to send actions to the
reducer, triggering state updates.

Benefits and Use Cases

React Reducer offers several benefits for state management in React


applications. It promotes a single source of truth, simplifies complex state
logic, and facilitates predictable state updates. It is particularly useful for
handling state in large-scale applications with multiple components and
intricate data flows.

Let’s see this in our application and make a cart reducer!

First as usual we need to install the package:


npm install react-redux

Make a folder store and create an another folder named cart. This folder
structure help us to organize better if we have multiple reducers.

Inside the cart folder first make the actionType.js where we can export the
action types. We need to add item to the cart, remove item from the cart and
just in case empty the hole cart.

export const ADD_TO_CART = "ADD_TO_CART";


export const REMOVE_ITEM = "REMOVE_ITEM";
export const EMPTY_CART = "EMPTY_CART";

In the cartActions.js define the reducer “skeleton”. The action creators are
functions that create and return action objects, which describe the type of
state change to be performed:

import * as actionTypes from "./actionTypes";


//receives the object
export const addToCart = (item) => {
return {
type: actionTypes.ADD_TO_CART,
item: item,
};
};

//receives the object id


export const removeFromCart = (id) => {
return {
type: actionTypes.REMOVE_ITEM,
id: id,
};
};

export const emptyCart = () => {


return {
type: actionTypes.EMPTY_CART,
};
};

These action creators can be used to dispatch actions within your Redux
application. Dispatching an action triggers the corresponding reducer
function, which updates the state based on the action type.

In the last file we define the reducer function that handle types of actions:
adding item to the cart, removing an item from the cart, and emptying the
cart.
import { ADD_TO_CART, REMOVE_ITEM, EMPTY_CART } from "./actionTypes";

const initState = {
addedItems: [],
total: 0,
};
const cartReducer = (state = initState, action) => {

if (action.type === ADD_TO_CART) {


console.log("acion", action);
let addedItem = action.item.product;
let itemAmount = action.item.amount;

//check if the action id exists in the addedItems


let existed_item = state.addedItems.find(
(item) => addedItem._id === item._id
);

if (existed_item) {
let updatedItem = { ...existed_item };
updatedItem.quantity =
parseInt(updatedItem.quantity) + parseInt(itemAmount);

// Create a new array for the modified addedItems


let updatedAddedItems = state.addedItems.map((item) =>
item._id === existed_item._id ? updatedItem : item
);

return {
...state,
addedItems: updatedAddedItems,
total: state.total + addedItem.price * itemAmount,
};
} else {
addedItem.quantity = parseInt(itemAmount);
//calculating the total
let newTotal = state.total + addedItem.price * itemAmount;

return {
...state,
addedItems: [...state.addedItems, addedItem],
total: newTotal,
};
}
} else if (action.type === REMOVE_ITEM) {

let existed_item = state.addedItems.find((item) => action.id === item._id);


if (existed_item.quantity > 1) {
let updatedItem = { ...existed_item };
updatedItem.quantity -= 1;

// Create a new array for the modified addedItems


let updatedAddedItems = state.addedItems.map((item) =>
item._id === existed_item._id ? updatedItem : item
);

return {
...state,
addedItems: updatedAddedItems,
total: state.total - existed_item.price,
};
} else {
let existed_item = state.addedItems.find(
(item) => action.id === item._id
);
let new_items = state.addedItems.filter((item) => action.id !== item._id);

//calculating the total


let newTotal = state.total - existed_item.price * existed_item.quantity;

return {
...state,
addedItems: new_items,
total: newTotal,
};
}
} else if (action.type === EMPTY_CART) {
return {
...state,
addedItems: [],
total: 0,
};
}
return state;
}
};

export default cartReducer;

Let’s take a moment to explore the inner workings of a Redux reducer


function designed to manage the state of a shopping cart. The cartReducer
function serves as the central hub for handling state updates related to our
shopping cart. It accepts two parameters: state, representing the current
state of the cart, and action, which dictates the specific action to be
performed. Within the cartReducer function, we define three action types:
ADD_TO_CART, REMOVE_ITEM, and EMPTY_CART. Each action type
triggers a specific set of operations to be executed on the cart’s state. For
instance, when the ADD_TO_CART action type is dispatched, the function
first checks if the item already exists in the addedItems array within the
cart’s state. If it does, the quantity of the existing item is incremented, and
the total is adjusted accordingly. However, if the item is not currently present
in the cart, it is added along with its respective quantity, and the total is
recalculated to reflect this change. Moving on to the REMOVE_ITEM action
type, the function locates the item in question within the addedItems array
based on its unique identifier. If the item’s quantity is greater than one, the
function decreases the quantity by one and updates the total accordingly. On
the other hand, if the item’s quantity is exactly one, it is removed from the
addedItems array, and the total is adjusted accordingly to reflect this change.
Lastly, the EMPTY_CART action type allows for clearing the entire cart.
When this action type is triggered, the addedItems array is emptied, and the
total is set back to zero. By utilizing this cartReducer function, we gain a
robust and structured approach to managing the state of our shopping cart.
It enables us to handle various actions consistently and ensures that our
cart’s state remains accurate and up to date.

Let’s use it!

Create a new page AddProductPage.js:

import React from "react";


import NavBar from "../components/NavBar";
import AddProduct from "../components/AddProduct";

const AddProductPage = () => {


return (
<React.Fragment>
<NavBar />
<AddProduct />
</React.Fragment>
);
};

export default AddProductPage;

Make things works update the ProductCard component and use our
reducers. Import the necessary actions and the useDispatch function.

import { addToCart, removeFromCart } from "../store/cart/cartActions";


import { useDispatch } from "react-redux";

The useDispatch function is a hook provided by the React-Redux library that


allows us to dispatch actions to the Redux store from within a functional
component. It is a fundamental tool for triggering state updates in a Redux
application. To use useDispatch, you first import it from the React-Redux
library. Then, within your functional component, you invoke useDispatch()
to get the dispatch function. This function can be assigned to a variable,
typically named dispatch, which you can then use to dispatch actions.

const dispatch = useDispatch();

Finnaly add the handleAddToCart function:

const handleAddToCart = (product) => {

const product_item = {
product: product,
amount: amountInputRef.current.value,
};
dispatch(addToCart(product_item));
};

In summary, the handleAddToCart function is triggered when a user wants to


add a product to the cart. It retrieves the entered quantity, creates an object
with the product details and quantity, and dispatches an action to update the
cart state in the Redux store.
Don’t forget to update also the UI component and use the function with the
button.

<Button
variant="contained"
color="primary"
endIcon={<AddShoppingCartIcon />}
last week
part-6
onClick={() => handleAddToCart(product)}
3 weeks ago
part 5
>
+ Add
</Button>
<TextField
inputRef={amountInputRef}
sx={{ width: 70 }}
label="Amount"
id={"amount_" + props.id}
type="number"
min={1}
max={5}
step={1}
defaultValue={1}
/>

Now we made our cart reducer but need to create the CartPage where we can
display our cart data.
The imports various components and icons from the Material-UI library, as
well as external dependencies like React, React Redux, React Router DOM,
and axios.

import React, { useEffect, useState } from "react";


import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { useContext } from "react";
import { AuthContext } from "../context/authContext";
import { Grid, Button, Paper, TableCell, TableContainer, TableRow, TableHead, Ta
import AddIcon from "@mui/icons-material/Add";
import RemoveIcon from "@mui/icons-material/Remove";
import Avatar from "@mui/material/Avatar";
import NavBar from "../components/NavBar";
import InputAdornment from "@mui/material/InputAdornment";
import AccountCircle from "@mui/icons-material/AccountCircle";
import HomeIcon from "@mui/icons-material/Home";
import FlagIcon from "@mui/icons-material/Flag";
import LocationCityIcon from "@mui/icons-material/LocationCity";
import MarkunreadMailbox from "@mui/icons-material/MarkunreadMailbox";
import LocalMallIcon from "@mui/icons-material/LocalMall";
import emptyCartImg from "../img/emptycart.png";

import Dialog from "@mui/material/Dialog";


import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogTitle from "@mui/material/DialogTitle";
The component uses the useState hook to define and manage local state
variables like totalAmount, open, accountDialog, showLogin, confirmShow,
and checkoutForm. It uses the useSelector hook from React Redux to access
the Redux store and retrieve values like addedItems (items in the cart) and
total (total amount). The useDispatch hook is used to get the dispatch
function for Redux actions.

const CartPage = () => {


const navigate = useNavigate();
const dispatch = useDispatch();
const authContext = useContext(AuthContext);
const addedItems = useSelector((state) => state.cartStore.addedItems);
const total = useSelector((state) => state.cartStore.total);
const [totalAmount, setTotalAmount] = useState(0);
const [open, setOpen] = React.useState(false);
const [accountDialog, setAccountDialog] = React.useState(false);
const [showLogin, setShowLogin] = React.useState(false);
const [confirmShow, setConfirmShow] = React.useState(false);
const [checkoutForm, setCheckoutForm] = useState({
firstName: "",
lastName: "",
address: "",
city: "",
country: "",
zipCode: "",
});

useEffect(() => {
if (total !== undefined) {
setTotalAmount(`$${total.toFixed(2)}`);
}
}, [total, addedItems, totalAmount]);

const goBack = () => {


navigate("/");
};

const cartItemRemoveHandler = (id) => {


dispatch(removeFromCart(id));
};

const cartItemAddHandler = (item) => {

const product_item = {
product: item,
amount: 1,
};
dispatch(addToCart(product_item));
};

const handleCheckout = async () => {


if (!authContext.token) {
setOpen(true);
} else {
setConfirmShow(true);
}
};

const handleFormInput = (e) => {


const { name, value } = e.target;
setCheckoutForm({ ...checkoutForm, [name]: value });
};

const handleGoToLogin = () => {


setShowLogin(true);
setAccountDialog(true);
setOpen(false);
};
const handleCreateAccount = () => {
setShowLogin(false);
setAccountDialog(true);
setOpen(false);
};

const handleCloseAccountDialog = async () => {


setAccountDialog(false);
setConfirmShow(true);
};

const handleCancel = () => {


setConfirmShow(false);
};

const handleConfirm = async () => {


const order = {
userID: localStorage.getItem("userId"),
firstName: checkoutForm.firstName,
lastName: checkoutForm.lastName,
address: checkoutForm.address,
city: checkoutForm.city,
country: checkoutForm.country,
zipCode: checkoutForm.zipCode,
totalAmount: totalAmount,
items: addedItems,
createdDate: new Date(),
};
try {
const response = await axios.post("http://localhost:5000/order/create", {
data: order,
});
if (response.data === "Order saved to the database!") {
dispatch(emptyCart());
setConfirmShow(false);
navigate("/");
}
} catch (e) {
console.log(e);
}
console.log(order);
};

You already guessed we need to define at the backend the new route for
saving the orders into the database. So let’s go to the backend side and take
the actions.

Define the order model:

const mongoose = require("mongoose");

const orderSchema = new mongoose.Schema({


userID: { type: String, required: true },
firstName: { type: String, required: true },
lastName: { type: String, required: true },
address: { type: String, required: true },
city: { type: String, required: true },
country: { type: String, required: true },
zipCode: { type: String, required: true },
totalAmount: { type: String, required: true },
items: { type: String, required: true },
createdDate: { type: Date, required: true },
});
const Order = mongoose.model("Order", orderSchema);

exports.Order = Order;

Add the order routes:

const express = require("express");


const mongoose = require("mongoose");
const { Order } = require("../Models/orders");
const router = express.Router();

//Post new order


//Create API
router.post("/create", async (req, res) => {
console.log(req.body);
const newOrder = new Order({
userID: req.body.data.userID,
firstName: req.body.data.firstName,
lastName: req.body.data.lastName,
address: req.body.data.address,
city: req.body.data.city,
country: req.body.data.country,
zipCode: req.body.data.zipCode,
totalAmount: req.body.data.totalAmount,
items: JSON.stringify(req.body.data.items),
createdDate: req.body.data.createdDate,
});

await Order.create(newOrder);
res.send("Order saved to the database!");
});
module.exports = router;

And don’t forget to update the index.js:

const order = require("./Routes/orders");


...
app.use("/order", order);

If are you here you already made the hard part. Before we can test it, we
must update our navbar:

import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";


import Badge from "@mui/material/Badge";
import { IconButton } from "@mui/material";
import { useSelector } from "react-redux";
import logo from "../img/logo_2.png";
import ButtonBase from "@mui/material/ButtonBase";
The useSelector function from the react-redux module is imported,
enabling the selection of data from the Redux store.

Add the navigate function:

const goToOrders = () => {


navigate("/cart");
};

And use with the ShoppingCartIcon:

<AppBar position="static" sx={{ background: "#38B6FF" }}>


<Toolbar>
<ButtonBase onClick={goToHome}>
<Box
component="img"
sx={{ width: "8rem", height: "5rem" }}
src={logo}
/>
</ButtonBase>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }} />

<IconButton onClick={goToOrders}>
<Badge badgeContent={items.length} color="secondary">
<ShoppingCartIcon />
</Badge>
</IconButton>
{isAdmin && (
<Button color="inherit" onClick={goToAddProduct}>
Add product
</Button>
)}
{!token ? (
<Button color="inherit" onClick={goToLogin}>
Login
</Button>
) : (
<Button color="inherit" onClick={logOut}>
LogOut
</Button>
)}
</Toolbar>
</AppBar>

Well done! The last thing is to add the page to the app.js:

import CartPage from "./pages/CartPage";


...
<Route path="/cart" element={<CartPage />} />

Nice job!
In conclusion, the reducer implemented in the order page provides a crucial
functionality for managing the state of the user’s shopping cart. By utilizing
the addToCart , removeFromCart , and emptyCart actions, the reducer enables
seamless addition, removal, and clearing of items from the cart. The use of
Redux and the useSelector hook allows efficient access to the cart's data
throughout the component. With the help of this reducer, the order page
becomes a powerful tool for users to review, modify, and finalize their
purchases, ensuring a smooth and enjoyable shopping experience.

In the upcoming section, we will be developing the order page, which will
allow logged-in users to view their past orders. See you in Part 7 for more
details.

React Reactjs Mern Stack Nodejs Ecommerce Software


Written by Tókos Bence Follow

43 Followers

Hi everyone! I'm an enthusiastic full-stack developer. Please feel free to reach out to me via
email (tokosbex@gmail.com) or Twitter (@tokosbex).

More from Tókos Bence

Tókos Bence Tókos Bence

Creating an E-commerce Site with Creating an E-commerce Site with


MERN Stack — Part VIII MERN Stack — Part VII
Hello and welcome to our ongoing journey! Welcome to Part 7! In this installment, we will
dive into the process of creating an order…
Jan 15 1 Jan 15 10

Tókos Bence Tókos Bence

Node js and GraphQL — Part 2 Empowering Development: Using


Introduction Multiple Node.js Versions on…
If you’ve worked with React or any Node.js
application, you’ve likely encountered…

Jan 15 8 1 Apr 25 2

See all from Tókos Bence


Recommended from Medium

Andrew Be… in Artificial Intelligence in Plain Engli… Whapi.Cloud API

You are Using ChatGPT Wrong! — How to send message to


#1 Mistake 99% of Users Make WhatsApp Channel via API
Prompt Engineering “Gurus” are Our Cloud API supports WhatsApp Channels,
recommending this same prompting mistak… a brand-new feature introduced by…

May 19 2.9K 44 Jan 11 9

Lists
Stories to Help You Grow as a General Coding Knowledge
Software Developer 20 stories · 1319 saves
19 stories · 1151 saves

data science and AI Medium's Huge List of


40 stories · 192 saves Publications Accepting…
312 stories · 2969 saves

Thanos Has San in DevOps.dev

How I Work 15 Minutes a Day on A Beginner’s Guide to Integrating


Etsy (And Make $1000/month) FullCalendar Library in React.js
If I told you that you could earn $1000 per Introduction:
month by spending only 15 minutes a day,…

Apr 30 1K 28 Feb 16 1
Sudhanshu Dubey in Stackademic Surya Teja

MERN: Creating a full stack MERN Stack Dev Roadmap


authentication system using JWT… Here’s a comprehensive roadmap to master
I am hoping you guys celebrated after the MERN stack, with specific learning…
finishing Part 1 of this article where we…

Feb 2 13 2 Jun 6

See more recommendations

Help Status About Careers Press Blog Privacy Terms Text to speech Teams

You might also like