I'm working on my first React project where I'm implementing a shopping cart feature. When a user clicks on a button to increase the quantity of an item in their cart, I update the quantity in the MongoDB database through my backend route (addCart). After updating the quantity in the database, I want to re-render the component to reflect the changes in the total price, but the state isn't updating accordingly.
//My states and their initial values
const [cartItems, setCartItems] = useState([]); //This currently holds multiple objects
const [totalPrice, setTotalPrice] = useState(0);
//Button that adds to the quantity
<button className='inline-flex items-center justify-center font-medium text-xl text-slate-600 mx-2 h-8 w-8 hover:border hover:bg-gray-300 hover:border-gray-300 hover:rounded-full' onClick={() => {handleAddCounter(item.productId, item.productImage, item.productTitle, item.productPrice, item.productSize, item.quantity)}}>+</button>
//Trying to run fetch data and calculate price on each re-render
useEffect(() => {
fetchCartData();
//Running calculate total price to get the new value from the state
calculateTotalPrice();
}, [cartItems]);
//Updating the quantity
const handleAddCounter = (prodId, img, title, price, size, quantity) => {
const updatedQuantity = quantity + 1;
quantityUpdater(prodId, img, title, price, size, quantity, updatedQuantity);}
//Sending that to the back end
const quantityUpdater = async (prodId, img, title, price, size, quantity, updatedQuantity) => {
try {
const token = localStorage.getItem('token');
console.log('Getting token from singleProduct: ', token);
const showUserName = username;console.log('Seeing if username has a value: ', showUserName);
const showProductId = prodId;console.log('Seeing if product id has a value: ', showProductId);
if (!token) {
throw new Error('Authentication token not found');
}
const response = await fetch('http://localhost:3001/auth/user/addCart', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
productId: prodId,
img: img,
title: title,
price: price,
size: size,
quantity: updatedQuantity,
username,
}),
});
if (!response.ok) {
throw new Error(`Network response error: ${response.statusText}`);
}
// Update cartItems state with the new data (forcing state to re-render because it doesn't recognize an updated object)
// React only recognizes a state change when the state is se t with a new object
const updatedCartItems = cartItems.map(item => {
if (item.productId === prodId && item.productSize === size) {
// Update quantity for matching product ID and size
return {
...item,
quantity: updatedQuantity
};
}
// Return unchanged item for other items
return item;
});
setCartItems(updatedCartItems);
calculateTotalPrice();
console.log('Calculate Total price fired!');
} catch (error) {
console.error('Error adding to cart: ', error.message);
}
};
//Calculating the total price
const calculateTotalPrice = () => {
console.log('Calculate total price received and running');
// Check if cart is defined and is an array
if (cartItems && Array.isArray(cartItems)) {
let addedValue = 0;
console.log('This is to check the calculated price to see if cartItems is actually printing something worth while: ', cartItems);
for (let i = 0; i < cartItems.length; i++) {
// Check if cart[i] is not undefined before accessing its properties
if (cartItems[i] && cartItems[i].productPrice) { addedValue += cartItems[i].productPrice; } }
setTotalPrice(addedValue);
console.log('Is total Price updating the price?: ', totalPrice);
} else {
setTotalPrice(0);
}
}
//Back end to create new item or update an existing object
router.post('/addCart', authenticate, async (req, res) => {
const { productId, img, title, price, size, quantity, username } = req.body;
//Extracting product information from
console.log('Product ID in route:', productId);
console.log('IMG in route:', img);
console.log('title in route:', title);
console.log('Price in route:', price);
console.log('Size in route:', size);
console.log('Quantity in route:', quantity);
console.log('username in route:', username);
// Extract productId from req.body
const isProductIdANum = req.body.productId; console.log('Type of productId:', typeof isProductIdANum);
//Price being manipulated below
let newPrice;
// Check if they exist, I don't want to go further if they don't
if (!productId || !img || !title || !price || !size || !quantity || !username) {
return res.status(400).json({ error: 'Missing required fields' });
}
try {
//Querying the username field in the user collection
const user = await User.findOne({ username });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
//Check for existing cart item
//The productId being produced from req.body is a string so we converted it back to int
const existingCartItem = user.cart.find(item => item.productId === parseInt(productId));
const existingSize = user.cart.find(item => item.productSize === parseInt(size));
if (existingCartItem && existingSize) {
//Changing the quantity value instead of adding another object
console.log('Existing item found!: ', existingCartItem);
//Storing the original price
const originalPrice = existingCartItem.productPrice / existingCartItem.quantity;
console.log('Original Price: ', originalPrice);
//updating the quantity
existingCartItem.quantity = quantity;
console.log('Quantity: ', quantity);
//Price Based on Quantity
newPrice = originalPrice * quantity;
console.log('New Price: ', newPrice)
existingCartItem.productPrice = newPrice;
} else {
// Adding the product to the cart
productPrice = price * quantity;
user.cart.push({ productId, productImage: img, productTitle: title, productPrice: productPrice, productSize: size, quantity: quantity });
}
await user.save();
res.status(201).json({ message: 'Product added successfully to cart' });
} catch (error) {
console.error('Error adding to cart: ', error.message);
res.status(500).json({ error: 'Could not add to cart' });
}
});
Since totalPrice can be directly calculated from the cart, it should not be its own state. Instead, calculate it during rendering. This will reduce the number of renders, make it impossible for the states to be out of sync, and as a bonus it will fix the issue you're running into where your calculation is using stale closure variables.
const [cartItems, setCartItems] = useState([]);
let totalPrice = 0;
if (cartItems && Array.isArray(cartItems)) {
for (let i = 0; i < cartItems.length; i++) {
if (cartItems[i] && cartItems[i].productPrice) {
totalPrice += cartItems[i].productPrice;
}
}
}
If the calculation is expensive, you can wrap it in useMemo
so it will only recalculate when the cartItems change.
const [cartItems, setCartItems] = useState([]);
const totalPrice = useMemo(() => {
let totalPrice = 0;
if (cartItems && Array.isArray(cartItems)) {
for (let i = 0; i < cartItems.length; i++) {
if (cartItems[i] && cartItems[i].productPrice) {
totalPrice += cartItems[i].productPrice;
}
}
}
return totalPrice;
}, [cartItems]);