user13353216
user13353216

Reputation:

Updated state values are not displaying in react

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

Answers (2)

Drew Reese
Drew Reese

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

  1. 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.

  2. 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".

  3. 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

Manan Joshi
Manan Joshi

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

Related Questions