UseState not updating correctly with an Await function inside iteration of an array

i have a little problem with updating the UI in React after fetching additional data from the server. The problem is that not all products are updating the price in the UI. I´m sure its because of the async function. Heres my code:

useEffect(() => {
    async function loadNotes() {
      console.log("loading notes");
      try {
        setShowNotesLoadingError(false);
        setNotesLoading(true);
        const products = await ProductsApi.fetchProducts();
        for (let index = 0; index < products.length; index++) {
          const prices = await ProductsApi.getLowestPrice(products[index].asin);
          products[index].lowestPriceFraction = prices.lastPriceFraction;
          products[index].lowestPriceWhole = prices.lastPriceWhole;
          setProducts(products);
        }
        setProducts(products);

      } catch (error) {
        console.error("Fehler: " + error);
        setShowNotesLoadingError(true);
      } finally {
        setNotesLoading(false);
      }
    }
    loadNotes();
  }, []);

Hope someone can help my to fix my little problem. Thank you

>Solution :

products[index].lowestPriceFraction = prices.lastPriceFraction;
products[index].lowestPriceWhole = prices.lastPriceWhole;

The problem is that these lines are mutating the array, and that array is in state (after the first loop anyway). Mutating state causes problems, because react expects it can to an === comparison between the old and new state to tell if it has changed. So when you call setProducts(products) react does its ===, sees that it’s the same array, and thus skips every render but the first.

Instead, every time you set state you must do so without mutation. In your case that means making a copy of the array, and making a copy of the specific item you want to change

const products = await ProductsApi.fetchProducts();
setProducts(products);
for (let index = 0; index < products.length; index++) {
  const prices = await ProductsApi.getLowestPrice(products[index].asin);
  // using function version of setProducts, so i have the latest state
  setProducts((prev) => {
    // Creating a new array
    return prev.map((val, i) => {
      if (index === i) {
        // Creating a new item
        return {
          ...val, 
          lowestPriceFraction: prices.lastPriceFraction,
          lowestPriceWhole: prices.lastPriceWhole
        }
      } else {
        return val;
      }
    });
  });
}

Btw, one way you could change this code is to get all the prices in parallel (thus reducing the total loading time) and just set state once at the end (thus simplifying the code):

const products = await ProductsApi.fetchProducts();
const promises = products.map(product => {
  return ProductsApi.getLowestPrice(product.asin)
});
const pricesArray = await Promise.all(promises);
pricesArray.forEach((prices, i) => {
  // I am mutating, but this data isn't in state yet so it won't hurt.
  products[i].lowestPriceFraction = prices.lastPriceFraction;
  products[i].lowestPriceWhole = prices.lastPriceWhole
});
setProducts(products);

Leave a Reply