0

I am implementing a React shopping cart, and I am having a locked loop problem with an useEffect hook.

Initially, I use an useEffect to fetch id's and quantities of products in the cart and store these values in a state variable array of objects named shopCart:

// fetches the id's and quantities of all products in the user's shopping cart
    // and assigns these values to the shopCart array
    // Each value of the shopCart array should be an object like this: {id:random_id, quantity:number_of_products }]
    useEffect(() => {
        let auxCart = []; // auxiliary array that will receive the result of the fetch request
        // postData: performs a POST request with {userId:userId} as the body of the request
        postData('REQUEST1_URL', { userId: userId })
            .then(data => {
                for (let i = 0; i < data.shopCart.length; i++) {
                    auxCart[i] = {};
                    auxCart[i].id = data.shopCart[i].id; //this shopCart is not the same shopCart state variable. Just a value received from the database
                    auxCart[i].quantity = data.shopCart[i].quantity;
                }
            });
        setShopCart(auxCart);
    }, [])

After that, I modify shopCart by adding attributes/keys with info about the products, such as product title, price and images:

   // Modifies shopCart, which should contain only id's and quantities at the moment, 
    //  assigning details to each product such as price and title.
    // Each value of shopCart should be an object like this:
    // {id:random_id, quantity:number_of_products,
    // title:product_name, price:product_price,
    // oldPrice:old_product_price, image:array_of_images,
    // type:type_of_product}
    useEffect(() => {
        console.log('useEffect2');
        if (!isFirstRender) { //Ref hook variable used to test if the useEffect is in the first render or not 
            let aux = [...shopCart]; // creates a copy of shopCart
            for (let i = 0; i < shopCart.length; i++) {
                // fetches the details of each product
                fetch('REQUEST2_URL').
                    then(res => res.json()).
                    then(res => {
                        aux[i].image = res[0].image;
                        aux[i].title = res[0].title;
                        aux[i].price = res[0].price;
                        aux[i].oldPrice = res[0].oldPrice;
                        aux[i].type = res[0].type;
                    });
            }
            setShopCart(aux);
        }
    }, [shopCart]);

The problem is that the second useEffect keeps printing 'useEffect2' multiple times, showing that it is in a locked loop. How can I fix that ? I was expecting that, since the fetched products info doesn't change, the second useEffect would not be repeating everytime, but it does. I use shopCart in its dependecies arrays to wait for the first request to finish updating shopCart.

2 Answers 2

2

That is happening because inside your second useEffect, you have shopCart in your dependency array and inside the function you set shopCart there, hence it runs infinitely.

You could do all this use case inside the first useEffect. How? You could await for the first fetch to respond and then you for the second request. (You could keep then() if you want, but IMO is much cleaner async...await way)

Leaving it like:

useEffect(async () => {
        let auxCart = []; // auxiliary array that will receive the result of the fetch request
        // postData: performs a POST request with {userId:userId} as the body of the request
        const data = await postData('REQUEST1_URL', { userId: userId })
                for (let i = 0; i < data.shopCart.length; i++) {
                    auxCart[i] = {};
                    auxCart[i].id = data.shopCart[i].id; //this shopCart is not the same shopCart state variable. Just a value received from the database
                    auxCart[i].quantity = data.shopCart[i].quantity;
                }
            let aux = [...shopCart]; // creates a copy of shopCart
            for (let i = 0; i < shopCart.length; i++) {
                // fetches the details of each product
                const res = await fetch('REQUEST2_URL').
                const resData = await res.json();
                        aux[i].image = resData[0].image;
                        aux[i].title = resData[0].title;
                        aux[i].price = resData[0].price;
                        aux[i].oldPrice = resData[0].oldPrice;
                        aux[i].type = resData[0].type;
            }
       // do whatever you want with those objects
    }, [])

To make the code much cleaner you could also get rid of those aux variables (seems much like imperative programming languages) and start using array functions like map, filter, etc. depending your need. Indeed in the first for loop you are just setting the same values to another array when you could directly use that array:

useEffect(async () => {
        const shopCart = await postData('REQUEST1_URL', { userId: userId }).shopCart
            let aux = [...shopCart]; // creates a copy of shopCart
            for (let i = 0; i < shopCart.length; i++) {
                // fetches the details of each product
                const res = await fetch('REQUEST2_URL').
                const resData = await res.json();
                        aux[i].image = resData[0].image;
                        aux[i].title = resData[0].title;
                        aux[i].price = resData[0].price;
                        aux[i].oldPrice = resData[0].oldPrice;
                        aux[i].type = resData[0].type;
            }
       // do whatever you want with those objects
    }, [])

And with the second for..loop you could also use a map but it's a little more complicated since you should use an async func inside the map and wait for all Promises to resolve using Promise.all() and it's just a whole new solution... Leaving it for you ;)

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks so much, mate. It's working. I had no idea you could use async await like that in useEffect. This worked so much better than the way I've been implementing things, that I am actually considering modifying a lot of the codes in this site I'm making. Thanks a bunch.
No problem at all! You could also do a map when doing the for loop since you don't need an auxCart variable, etc. Editing my answer right now
1

There are 2 things creating the infinite loop in your code. With these 2 things combined, it creates the loop.

let aux = [...shopCart]; // creates a copy of shopCart

<= This action create a plain new object, therefore a new shopCart is always created, making React think it's a different object, even when the content might be the same

setShopCart(aux);
    }
}, [shopCart]);

<= You are setting this new object to shopCart, and as React always think this is a new object, it always rerun the useEffect

How to fix this?

I'm not sure why you split into 2 different useEffect and adding the shopCart as dependency in the 2nd useEffect, but based on what you describe, you can always combine them into one useEffect, and use Promise or async/await to make sure the 2nd API call is only run after the 1st API call finishes. For more detail on this, I think Agustin posted a pretty good example one.

1 Comment

Yes, I was thinking that I had to use the dependency on the second useEffect as a way to wait for shopCart to finish the update in the first useEffect so that I could start the 2nd update. But I used the example with async/await Agustin provided, and it worked much better. I had no idea about this way of implementing things, and it expanded my horizons. Thank you, Jake, for also pointing me in a better direction.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.