Reputation:
I am implementing a shopping cart in MERN. Once user increment or decrement the quantity of a product, the total price should update accordingly. (The increment and decrement buttons are in a child component). Once I click on increment or decrement button, it is updating the mongoDB successfully. From the both increment
and decrement
functions, updated Cart will get as the response and it will set to the state's Cart variable. Then updateStates
method will trigger and updates the state variables (total & discount) according to the new Cart values. total
& discount
states are used to display the bill within this component (I have put some comments to find out). But the problem is, when I increment or decrement, it will not display updated bill value until I refresh the page. I want to update the values without refreshing the page manually. How can I fix this issue? Thanks in advance!
import React, { useEffect, useState, Component} from 'react';
import Titles from "../Titles";
import CartColumns from './CartColumns';
import EmptyCart from "./EmptyCart";
import CartList from "./CartList";
import CartTotals from "./CartTotals"
import NavBar from "../NavBar";
import {connect} from "react-redux";
import {Link, Redirect} from "react-router-dom";
import Axios from "axios";
const mapStateToProps = ({ session}) => ({
session
});
class Cart extends Component {
constructor() {
super();
this.increment = this.increment.bind(this);
this.decrement = this.decrement.bind(this);
this.removeItem = this.removeItem.bind(this);
}
state = {
Cart: [],
total : 0,
discount: 0
};
componentDidMount() {
if(this.props.session.userId !== null){
this.getItems();
}
}
//get cart items of the user and set to the Cart variable in state
getItems = () => {
Axios.get('http://localhost:5000/api/cart/getCart', {params:{userId: this.props.session.userId}})
.then(res => {
const cart = res.data;
let tempProducts = [];
cart.forEach(item => {
const singleItem = {...item};
tempProducts = [...tempProducts, singleItem];
});
this.setState({
Cart : tempProducts
});
this.updateStates();
})
};
//this will update state values once increment or decrement happens
updateStates = () => {
this.setState({
total : 0,
discount : 0
});
this.state.Cart.forEach(item => {
this.setState({
total : this.state.total + (item.price * item.quantity),
discount : this.state.discount + (item.discount * item.quantity)
})
})
};
//Increment the quantity of the product.
increment = (productId) => {
const item = {
userId : this.props.session.userId,
productId: productId
};
Axios.post('http://localhost:5000/api/cart/increment', item)
.then(res=>{
if(res.status === 200){
console.log('Incremented');
const cart = res.data;
let tempProducts = [];
cart.forEach(item => {
const singleItem = {...item};
tempProducts = [...tempProducts, singleItem];
});
this.setState({
Cart : tempProducts
});
this.updateStates();
}
});
};
//decrement the quantity of the product
decrement = (productId) => {
const product = this.state.Cart.find(item => item.id === productId );
if(Number(product.quantity) > 1){
const item = {
userId : this.props.session.userId,
productId: productId
};
Axios.post('http://localhost:5000/api/cart/decrement', item)
.then(res=>{
if(res.status === 200){
console.log('Decremented');
const cart = res.data;
let tempProducts = [];
cart.forEach(item => {
const singleItem = {...item};
tempProducts = [...tempProducts, singleItem];
});
this.setState({
Cart : tempProducts
})
//this.updateStates();
}
});
}
};
removeItem = (productId) => {
//removes an item
};
render() {
if(this.props.session.username !== null){
return (
<section>
<NavBar />
{(this.state.Cart.length > 0) ? (
<React.Fragment>
<Titles name ="Your " title = "Cart">Cart</Titles>
<CartColumns/>
<CartList cart = {this.state.Cart} increment={this.increment} decrement={this.decrement} removeItem={this.removeItem}/>
{/*<CartTotals cart={this.state.Cart}/>*/}
<div className="container">
<div className="row">
<div className="col-10 mt-2 ml-sm-5 ml-md-auto col-sm-8 text-capitalize text-right">
<h5>
<span className="text-title">Sub Total: </span>
//display the total from state
<strong>$ {this.state.total}</strong>
</h5>
<h5>
<span className="text-title">Discount: </span>
//display the discount from the state
<strong>$ {this.state.discount}</strong>
</h5>
<h5>
<span className="text-title">Total: </span>
//display the sub total by calculating the values
<strong>$ {this.state.total - this.state.discount}</strong>
</h5>
<Link to='/'>
<div className="col-10 mt-2 ml-sm-5 ml-md-auto col-sm-8 text-capitalize text-right">
<button className="btn btn-outline-danger text-uppercase mb-3 px-5" type="button"
o>Clear Cart</button>
<button className="btn btn-outline-info ml-3 text-uppercase mb-3 px-5" type="button"
>Check Out</button>
</div>
</Link>
</div>
</div>
</div>
</React.Fragment>
) : (
<EmptyCart/>
)
}
</section>
);
} else {
return(
<Redirect to="/login" />
);
}
}
}
export default connect(
mapStateToProps
)(Cart);
Upvotes: 0
Views: 2322
Reputation: 202696
I think you've missed how state updates in react are queued and reconciled. React state updates are asynchronous and batched processed between render/commit phases.
Issues
Your major error is queuing up a state update of this.state.Cart
then immediately queuing updates within the same cycle to aggregate on the new cart. When you do this in updateState
this.state.Cart
is still the cart value from the current render cycle the update was queued in.
A second, nearly as major, error also occurs in updateStates
when you forEach
over the cart and queue up state updates using this.state.total|discount
with the same reason. this.state.total|discount
will be the same value each time so only the last state set will "stick".
The third "strike" and reason you need to reload the page is because you only compute the cart on componentDidMount
and because of the above your code never actually updates the cart contents and thus re-renders the same content.
Solution - Use componentDidUpdate
lifecycle function and more optimal state updates
Good news is, updating the cart isn't an issue in getItems
, increment
, or decrement
, this works fine
this.setState({
Cart: tempProducts
});
But this should be the end of the function. Time to let react lifecycle work for you!
Implement componentDidUpdate
. This essentially will replace your updateState
function.
componentDidUpdate(prevProps, prevState) {
// Check if Cart updated
if (prevState.Cart !== this.state.Cart) {
// It did! Now compute new total and discount state
}
}
Implement more optimal state update. In your code, queuing up multiple updates is ok, but you need to use a functional state update since each update depends on the previous state.
this.setState({ total: 0, discount: 0 });
this.Cart.forEach(({ discount, price, quantity }) => {
this.setState(prevState => ({
total: prevState.total + (price * quantity),
discount: prevState.discount + (discount * quantity),
}));
});
So the updated code now is
componentDidUpdate(prevProps, prevState) {
// Check if Cart updated
if (prevState.Cart !== this.state.Cart) {
// It did! Now compute new total and discount state
this.setState({ total: 0, discount: 0 });
this.Cart.forEach(({ discount, price, quantity }) => {
this.setState(prevState => ({
total: prevState.total + (price * quantity),
discount: prevState.discount + (discount * quantity),
}));
});
}
}
This queues up many updates though, and all you really want to do is replace the total and discount values. You can aggregate those before and set state once with the new values by iterating over the cart and reducing the total and discount into a single (object) value. You no longer need to worry about previous state.
componentDidUpdate(prevProps, prevState) {
// Check if Cart updated
if (prevState.Cart !== this.state.Cart) {
// It did! Now compute new total and discount state
const totalAndDiscount = this.Cart.reduce(
({ total, discount }, item) => ({
total: total + item.price * item.quantity,
discount: discount + item.discount * item.quantity
}),
{ total: 0, discount: 0 }
);
this.setState(totalAndDiscount);
}
}
Upvotes: 2
Reputation: 679
You are running into something that is called a stale closure. To update state efficiently you should not update state inside a loop rather try doing the following
//this will update state values once increment or decrement happens
updateStates = () => {
let total = 0;
let discount = 0;
this.state.Cart.forEach(item => {
total = total + (item.price * item.quantity);
discount = discount + (item.discount * item.quantity);
})
this.setState({
total : total,
discount : discount
});
};
Upvotes: 0