Ashutosh Tiwari
Ashutosh Tiwari

Reputation: 1597

How can I wait for the loop to Complete?

Let me first show you what the code looks like

 Cart.getCompleteCart((cart)=>{
    
       let products = []; *//this Array has to be filled*

       totalPrice = cart.totalPrice;

       for(let prod of cart.products){

                Product.getProductDetails(prod.productId, productDetails => {
                    products.push({
                        product: productDetails,
                        productCount: prod.productCount
                    });
                });

      }
      console.log("Products are ->", products); *//this line is running before for loop!*
 }
       

console.log() is running before the for loop completes its work.

How to run "for loop" kind of in sync with the console.log() ?

Upvotes: 6

Views: 9808

Answers (2)

Gershom Maes
Gershom Maes

Reputation: 8131

Promise.all is designed for pretty much this exact use-case:

// A dummy "Product" with a dummy "getProductDetails" implementation
// so that we can have a working test example
let Product = {
  getProductDetails: (productId, callback) => {
    setTimeout(() => {
      callback({ type: 'productDetails', productId });
    }, 100 + Math.floor(Math.random() * 200));
  }
};

// This is the function you're looking for:
let getCompleteCart = async (cart) => {
  
  return Promise.all(cart.products.map(async ({ productId, productCount }) => ({
    product: await new Promise(resolve => Product.getProductDetails(productId, resolve)),
    productCount
  })));
  
}

let exampleCart = {
  products: [
    { productId: 982, productCount: 1 },
    { productId: 237, productCount: 2 },
    { productId: 647, productCount: 5 }
  ]
};
getCompleteCart(exampleCart).then(console.log);

A breakdown of getCompleteCart:

let getCompleteCart = async (cart) => {
  
  return Promise.all(cart.products.map(async ({ productId, productCount }) => ({
    product: await new Promise(resolve => Product.getProductDetails(productId, resolve)),
    productCount
  })));
  
}

Promise.all (mdn) is purpose-built to wait for every promise in an Array of promises to resolve

We need to supply an Array of Promises to Promise.all. This means we need to convert our Array of data (cart.products) to an Array of Promises; Array.protoype.map is the perfect tool for this.

The function provided to cart.products.map converts every product in the cart to an Object that looks like { product: <product details>, productCount: <###> }.

The tricky thing here is getting the value for the "product" property, since this value is async (returned by a callback). The following line creates a promise that resolves to the value returned by Product.getProductDetails, using the Promise constructor (mdn):

new Promise(resolve => Product.getProductDetails(productId, resolve))

The await keyword allows us to convert this promise into the future value it results in. We can assign this future value to the "product" attribute, whereas the "productCount" attribute is simply copied from the original item in the cart:

{
  product: await new Promise(resolve => Product.getProductDetails(productId, resolve)),
  productCount
}

In order to run console.log at the right time we need to wait for all product details to finish compiling. Fortunately this is handled internally by getCompleteCart. We can console.log at the appropriate time by waiting for getCompleteCart to resolve.

There are two ways to do this:

Using Promise.prototype.then (mdn):

getCompleteCart(exampleCart).then(console.log);

Or, if we're in an async context we can use await:

let results = await getCompleteCart(exampleCart);
console.log(results);

Upvotes: 2

Ernesto Stifano
Ernesto Stifano

Reputation: 3130

I assume that Product.getProductDetails() is an async method (it looks like a query to an API).

console.log() is not "running before the for loop", it is just that the tasks performed by Product.getProductDetails() are being handled asynchronously.

Since it also seems that the Product.getProductDetails() method works with callbacks, one way to achieve your goal would be to promisify each call and then condensate all the promises into a single one by using Promise.all() method.

Something like this should do the trick:

Cart.getCompleteCart((cart) => {
    const promises = [];

    for (let prod of cart.products) {
        promises.push(
            new Promise((resolve) => {
                Product.getProductDetails(prod.productId, (productDetails) => {
                    resolve({
                        product: productDetails,
                        productCount: prod.productCount
                    });
                });
            })
        );
    }

    Promise.all(promises).then((products) => {
        console.log('Products are ->', products);
    });
});

Or:

Cart.getCompleteCart((cart) => {
    Promise.all(
        cart.products.map((prod) => {
            return new Promise((resolve) => {
                Product.getProductDetails(prod.productId, (productDetails) => {
                    resolve({
                        product: productDetails,
                        productCount: prod.productCount
                    });
                });
            });
        })
    ).then((products) => {
        console.log('Products are ->', products);
    });
});

Upvotes: 9

Related Questions