Search code examples
javascriptreactjslocal-storage

Only one item is added in state when adding multiple with multiple setState calls


For learning purposes, I'm creating an e-shop, but I got stuck with localStorage, useEffect, and React context. Basically, I have a product catalog with a button for every item there that should add a product to the cart.

It also creates an object in localStorage with that item's id and amount, which you select when adding the product to the cart.

My context file:

 import * as React from 'react';

 const CartContext = React.createContext();

 export const CartProvider = ({ children }) => {
   const [cartProducts, setCartProducts] = React.useState([]);

 const handleAddtoCart = React.useCallback((product) => {
   setCartProducts([...cartProducts, product]);
   localStorage.setItem('cartProductsObj', JSON.stringify([...cartProducts, product]));
 }, [cartProducts]);

 const cartContextValue = React.useMemo(() => ({
   cartProducts,
   addToCart: handleAddtoCart, // addToCart is added to the button which adds the product to the cart

 }), [cartProducts, handleAddtoCart]);

 return (
   <CartContext.Provider value={cartContextValue}>{children}</CartContext.Provider>
   );
 };

export default CartContext;

When multiple products are added, then they're correctly displayed in localStorage. I tried to log the cartProducts in the console after adding multiple, but then only the most recent one is logged, even though there are multiple in localStorage.

My component where I'm facing the issue:

const CartProduct = () => {
  const { cartProducts: cartProductsData } = React.useContext(CartContext);
  const [cartProducts, setCartProducts] = React.useState([]);

  React.useEffect(() => {
    (async () => {
      const productsObj = localStorage.getItem('cartProductsObj');
      const retrievedProducts = JSON.parse(productsObj);

      if (productsObj) {
        Object.values(retrievedProducts).forEach(async (x) => {
          const fetchedProduct = await ProductService.fetchProductById(x.id);
          setCartProducts([...cartProducts, fetchedProduct]);
        });
      }
    }
  )();
 }, []);

 console.log('cartProducts', cartProducts);

 return (
   <>
    <pre>
      {JSON.stringify(cartProductsData, null, 4)}
    </pre>
   </>
  );
 };

 export default CartProduct;

My service file with fetchProductById function:

const domain = 'http://localhost:8000';
const databaseCollection = 'api/products';
const relationsParams = 'joinBy=categoryId&joinBy=typeId';

const fetchProductById = async (id) => {
  const response = await fetch(`${domain}/${databaseCollection}/${id}?${relationsParams}`);
  const product = await response.json();

  return product;
};

const ProductService = {
  fetchProductById,
};

export default ProductService;

As of now I just want to see all the products that I added to the cart in the console, but I can only see the most recent one. Can anyone see my mistake? Or maybe there's something that I missed?


Solution

  • This looks bad:

    Object.values(retrievedProducts).forEach(async (x) => {
      const fetchedProduct = await ProductService.fetchProductById(x.id);
      setCartProducts([...cartProducts, fetchedProduct]);
    });
    

    You run a loop, but cartProducts has the same value in every iteration

    Either do this:

    Object.values(retrievedProducts).forEach(async (x) => {
      const fetchedProduct = await ProductService.fetchProductById(x.id);
      setCartProducts(cartProducts => [...cartProducts, fetchedProduct]);
    });
    

    Or this:

    const values = Promise.all(Object.values(retrievedProducts).map(x => ProductService.fetchProductById(x.id)));
    setCartProducts(values)
    

    The last is better because it makes less state updates