State not updating correctly when calling setter multiple times inside a loop in React

Viewed 78

I wan to send multiple objects to cart in one click. For that I'm looping an array and calling addProductToCart but it saves only one last object in CartContext state.

When sending objects one by one (from different pages, or some objects from same page if sending by one, but not all) it's save in state of CartContext.

When I test this I set delay when looping addProductToCart and saw that every object where in CartContext state, but next object overwrites previous object.

import React from "react";
import { useContext } from "react";
import { CartContext } from "../../contexts/cart.context";
import Button, { BUTTON_TYPE_CLASSES } from "../buttons/buttons.component";
import {
    BuyButtonCartSymbol,
    BuyButtonCartTitle,
    CurrentPrice,
    CurrentPriceCont,
    CurrentPriceSymbol,
    OldPrice,
    OldPriceCont,
    OldPriceSymbol,
    PriceDetailCont,
    QuantityOldPrice,
    Quantity,
} from "./priceDetail.style";

const PriceDetail = (...props) => {
    const [object] = props;
    const { addItemToCart } = useContext(CartContext);

    const handleOptionsFocus = () => {
        object.setFocusClicked(true);
    };

    let oldPriceSum = 0;
    let newPriceSum = 0;

    // Sum old prices
    for (const value of object.arrayOfObjectsSelected) {
        if (value.oldPrice) {
            oldPriceSum += value.oldPrice;
        } else {
            oldPriceSum += value.price;
        }
    }

    // Sum current prices
    for (const value of object.arrayOfObjectsSelected) {
        newPriceSum += value.price;
    }

    //Multiple objects sending to cart
    var testArr = object.arrayOfObjectsSelected;

    const addProductToCart = () => {
        testArr.forEach((prod, ind) => {
            console.log(ind, prod);
            addItemToCart(prod);
        });
    };

    return (
        <PriceDetailCont>
            <QuantityOldPrice>
                {oldPriceSum !== newPriceSum && (
                    <OldPriceCont>
                        <OldPrice>{oldPriceSum.toLocaleString("fr-FR")}</OldPrice>
                        <OldPriceSymbol> &#8381;</OldPriceSymbol>
                    </OldPriceCont>
                )}
                {object.selectedOptions.length > 1 && (
                    <Quantity onClick={handleOptionsFocus}>
                        [ {object.selectedOptions.length}{" "}
                        {object.selectedOptions.length < 5 ? "pred" : "predov"} ]
                    </Quantity>
                )}
            </QuantityOldPrice>
            <CurrentPriceCont>
                <CurrentPrice>{newPriceSum.toLocaleString("fr-FR")}</CurrentPrice>
                <CurrentPriceSymbol> &#8381;</CurrentPriceSymbol>
            </CurrentPriceCont>
            <Button
                buttonType={BUTTON_TYPE_CLASSES.buy}
                type="button"
                onClick={addProductToCart}
            >
                <BuyButtonCartSymbol />
                <BuyButtonCartTitle>to Cart</BuyButtonCartTitle>
            </Button>
            <Button buttonType={BUTTON_TYPE_CLASSES.oneClick} type="button">
                Instant buy
            </Button>
        </PriceDetailCont>
    );
};

export default PriceDetail;
import { createContext, useState } from "react";

// Helper function to add item to cart with conditions
const addCartItem = (cartItems, productToAdd) => {
    const existingCartItem = cartItems.find(
        (cartItem) => cartItem.pid === productToAdd.pid
    );

    if (existingCartItem) {
        return cartItems.map((cartItem) =>
            cartItem.pid === productToAdd.pid
                ? { ...cartItem, quantity: cartItem.quantity + 1 }
                : cartItem
        );
    }

    return [...cartItems, { ...productToAdd, quantity: 1 }];
};

export const CartContext = createContext({
    cartItems: [],
    addItemToCart: () => {},
});

export const CartProvider = ({ children }) => {
    const [cartItems, setCartItems] = useState([]);

    const addItemToCart = (productToAdd) => {

        setCartItems(addCartItem(cartItems, productToAdd));
    };

    const value = { cartItems, addItemToCart };

    return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
};
1 Answers

Issue

When looping an array and calling a state setter for each iteration, or when you simply call it multiple times in a row, React would use what's called Automatic Batching, and groups changes in one render.

To illustrate I created the below working example. We would expect to have state equal to 3 after clicking the button, but it will be 1 as it uses only the last setState(state + 1).

function App() {
  const [state, setState] = React.useState(0);
  return (
    <div>
      <button
        onClick={() => {
          setState(state + 1);
          setState(state + 1);
          setState(state + 1);
        }}
      >
        Update
      </button>
      state : {state}
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(<App />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="root"></div>

Solution

To have Automatic Batching and still the expected behaviour, you can use the function state updater, like so:

function App() {
  const [state, setState] = React.useState(0);
  return (
    <div>
      <button
        onClick={() => {
          setState(prev => prev + 1);
          setState(prev => prev + 1);
          setState(prev => prev + 1);
        }}
      >
        Update
      </button>
      state : {state}
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(<App />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="root"></div>

For your case, to have the same behaviour as above, change how you are calling setCartItems in addItemToCart in your context. Like so:

setCartItems(prevCartItems => addCartItem(prevCartItems, productToAdd));
Related