Search code examples
reactjsaxiosuse-effectshopping-cartuse-state

Initial Quantity and Price in Shopping Cart React.js


So I'm Building an react Shopping cart.I was able to add Total quantity and Total price functionalities but not able to display the initial price and quantity of products present in the cart.The products are fetched from backend using axios and storing it using useState.

Here's the code

const CartPage = (props) => {
  const [cartProducts, setCartProducts] = useState([]);
  const [totalQuantity,setTotalQuantity] = useState();
  const [totalPrice,setTotalPrice] = useState(0);
  const [loading,setLoading]= useState(false);

  const { enqueueSnackbar,closeSnackbar} = useSnackbar();

  const authCtx = useContext(AuthContext)
  const token = authCtx.token;
  const userId = authCtx.userId;



  useEffect(() => {
    setLoading(true)
    let queryParams = '?auth=' + token + '&orderBy="userId"&equalTo="' + userId + '"';
    axiosInstance.get('/Cart.json'+queryParams).then((response) => {
      //console.log(response.data);
      let fetchProductData = [];
      for (let key in response.data) {
        fetchProductData.push({
          ...response.data[key],
          productId: key,
        });
      }

      //console.log(fetchProductData);
      setCartProducts(fetchProductData);
      setLoading(false)
    });
  },[token,userId]);
     

Here's the totalPrice and Total Quantity Functionalities and they are called in increment & decrement counter handler

 const totalQuantityHandler=()=>{
      const totalQuantityCount=cartProducts.reduce((total,product)=>{
        return total+product.quantity;
      },0)
      //console.log(totalQuantityCount)
      setTotalQuantity(totalQuantityCount)
      //console.log(cartProducts)
    }
    
    const totalPriceHandler=()=>{
      const totalPriceCount=cartProducts.reduce((total,product)=>{
        return total+product.price*product.quantity;
      },0)
      //console.log(totalPriceCount);
      setTotalPrice(totalPriceCount);
    }

const incrementCounterHandler = (index) =>{
  const updatedCart = [...cartProducts]
  updatedCart[index].quantity++
  setCartProducts(updatedCart);
  totalQuantityHandler();
  totalPriceHandler();
  //console.log(cartProducts)
}

const decrementCounterHandler=(index)=>{
  const updatedCart = [...cartProducts]
  updatedCart[index].quantity--
  setCartProducts(updatedCart);
  totalQuantityHandler();
  totalPriceHandler();
}

So here is the JSX Part Of the code to display the total Quantity and total Price

 <React.Fragment>
    {cartProducts.map((product, index) => {
      //console.log(product)
      return (
        <CartCard
          key={product.id}
          Image={product.productImage}
          Title={product.productName}
          Price={product.price}
          Details={product.productDetails}
          removeFromCart={() => removeFromCartHandler("REMOVE",product.productId)}
          moveToFavorites={(event)=>moveToFavoritesHandler(event,product)}
          Quantity={product.quantity}
          incrementCounter={() => incrementCounterHandler(index)}
          decrementCounter={() => decrementCounterHandler(index)}
          onHandleCallBack={(value) => sizeChangeHandler(value,index)}
        />
      );
    })}
    <Divider style={{ height: "2px", backgroundColor: "#000" }} />
    <Box
      display="flex"
      alignItems="center"
      justifyContent="flex-end"
      padding="10px"
      margin="20px"
      marginBottom="0px"
    >
      <Typography variant="h6">
        SubTotal({totalQuantity} Items): Rs.{totalPrice}
      </Typography>
    </Box>
    <ButtonGroup cancelled={()=>cartPageCancelledHandler()} continued={()=>cartPageContinuedHandler()}/>
  </React.Fragment>

I tried many things but it doesnt seem to work. Is there a better way of implementing it?


Solution

  • Issue

    The issue here is that React state updates are asynchronously processed, so when you enqueue an update to the cartProducts state (i.e. setCartProducts(updatedCart);) it isn't using what the state will be updated to and instead uses the state from the current render cycle.

    I was able to add Total quantity and Total price functionalities but not able to display the initial price and quantity of products present in the cart. The products are fetched from backend using axios and storing it using useState.

    You don't call the utility functions after fetch your data, so the initial totalQuantity and totalPrice state isn't computed.

    Additionally, it seems you are also mutating your cart state when incrementing/decrementing the quantity.

    const incrementCounterHandler = (index) =>{
      const updatedCart = [...cartProducts]
      updatedCart[index].quantity++ // <-- mutation
      setCartProducts(updatedCart);
      totalQuantityHandler();
      totalPriceHandler();
      //console.log(cartProducts)
    }
    
    const decrementCounterHandler=(index)=>{
      const updatedCart = [...cartProducts]
      updatedCart[index].quantity-- // <-- mutation
      setCartProducts(updatedCart);
      totalQuantityHandler();
      totalPriceHandler();
    }
    

    Solution

    Use an useEffect hook with dependency on cartProducts array to compute the quantity and price state when cartProducts updates and remove the calls to the utility function from the cart updater functions. This way when the cartProducts state is initialized from the other mounting effect, and any other time the cart state is updated, the quantity and price will be recomputed.

    useEffect(() => {
      totalQuantityHandler();
      totalPriceHandler();
    }, [cartProduces]);
    

    You must shallow copy all state, and nested state, that is being updated.

    const incrementCounterHandler = (index) => {
      setCartProducts(cart => cart.map((item, i) => i === index
        ? { ...item, quantity: item.quantity + 1 }
        : item
      );
    };
    
    const decrementCounterHandler = (index) => {
      setCartProducts(cart => cart.map((item, i) => i === index
        ? { ...item, quantity: item.quantity - 1 }
        : item
      );
    };
    

    In fact, because these two functions are essentially identical, I prefer, and suggest, to combine them and pass in the quantity delta. I also suggest using a curried function to save needing to use an anonymous function when attaching the callback.

    const incrementCounterHandler = (index, value) => () => {
      setCartProducts(cart => cart.map((item, i) => i === index
        ? { ...item, quantity: item.quantity + value }
        : item
      );
    };
    

    Usage:

    <CartCard
      ...
      incrementCounter={incrementCounterHandler(index, 1)}
      decrementCounter={decrementCounterHandler(index, -1)}
      ...
    />
    

    I would also suggest not storing the derived quantity and price values in state. These should be computed per render from the actual state. If you are concerned about recomputing when the CartPage rerenders but the cartProducts state has not, then you can memoize these values.

    const { totalQuantity, totalPrice } = useMemo(() => {
      return cartProducts.reduce(({ totalQuantity, totalPrice }, { price, quantity }) => ({
          totalQuantity: totalQuantity + quantity,
          totalPrice: totalPrice + (quantity * price),
        }), {
          totalQuantity: 0,
          totalPrice: 0,
        });
    }, [cartProducts]);