fetch json in a For loop run after the loop is completed JS

Viewed 542

I'm fetching JSON, it returns multiple items so I use For loop to access these items, on each item, I need to fetch another json but this it run when the loop is completed. Please see code below:

fetch(url).then(function(response){
        response.json().then(function(data) {
            //do something
            for (item of data.list){ //list is the key of JSON returned
                //do something
                for (key in item){
                    if (key == "id"){
                        //do something
                        console.log(item.id);
                    } else {
                        //do something
                        console.log('other item key');
                        if (key == "name"){
                            fetch(url + '/' + item.id).then(function(response){
                                response.json().then(function(data) {
                                    //do something
                                    console.log('fetch item: ' + key);
                                });
                            });
                        } else if (key == "address"){
                            fetch(url + '/' + item.address).then(function(response){
                                response.json().then(function(data) {
                                    //do something
                                    console.log('fetch item: ' + key);
                                });
                            });
                        } else if (key == "city"){
                            fetch(url + '/' + item.city).then(function(response){
                                response.json().then(function(data) {
                                    //do something
                                    console.log('fetch item: ' + key);
                                });
                            });
                        }
                    }
                }
            }
            console.log('loop end');
        });
    });

Expected result:

0
other item key
fetch item: name
fetch item: address
fetch item: city
loop end

But I get this

0
other item key
loop end
fetch item: city
fetch item: city
fetch item: city

Please help! Thank you so much.

3 Answers

If I understand correctly, you are trying to have something execute after all your fetching completes. My favorite way to do this is using Promises and the async await syntax. I rewrote your code trying to flatten things out, and it also will now wait until the loops are done before logging "loop end".

The first thing I do is wrap my entire action in an anonymous async function that is immediately called.

The reason I use async, is because it is needed to use await. Now instead of using .then chains, I can tell the code to wait for the fetch to complete before continuing. This works on not only the fetch which returns a promise, but the .json method that returns a promise as well.

The for of loop is similar to yours, but I use the const keyword to prevent changing the value of a global variable.

I assume there is always an id, and would have used id actions without the need of checking to see if there is an id.

Instead of going through each key and having an if statement which executes code when a certain key is found, I loop through the desired targets and skip executing the code if the key does not exist.

If you need to do something different for any of the keys, you can remove them from the loop and just repeat it, but it gets pretty wet. Anything that is repeated should be put into a function or loop to only be written once

If you have any questions please ask!

(async () => {
  const response = await fetch(url);
  const data = await response.json();
  for (const item of data.list) {
    //do something with id?
    for (const key of ["name", "address", "city"]) {
      if (!(key in item)) continue;
      const keyRes = await fetch(`${url}/${item[key]}`);
      const keyData = await keyRes.json();
      //do something
      console.log(`fetch item: ${key}`);
    }
  }
  console.log("loop end");
})();

You mentioned that the fetches are taking too long happening in order, to give you an example of doing this without blocking in the loop, but blocking until all loop promises resolve, here is another example.

Here we write an async function processKey to handle the processing with async await syntax. Async function always return a promise, and the await keyword will wait for the promise to resolve. This way, we can call the function, but instead of waiting for it to resolve, we can store the promise in an array.

After we initiate all the promises without waiting for them to resolve, we can then use await Promise.all(promisesToWaitFor) and wait until all the promises in the array resolve before executing the last line of code.

It should be pointed out that we are saving time by no longer waiting for code in the loop to execute. Because of this, if one fetch takes less time than another, we will have actions in the loop happening "out of order". Depending on your use case, this may or may not be ideal.

If you need any clarification of the code, please ask!

async function processKey(key, url, item) {
  const keyRes = await fetch(`${url}/${item[key]}`);
  const keyData = await keyRes.json();
  //do something
  console.log(`fetch item: ${key}`);
}
const url = "Something";
(async () => {
  const response = await fetch(url);
  const data = await response.json();
  const promisesToWaitFor = [];
  for (const item of data.list) {
    //do something with id?
    for (const key of ["name", "address", "city"]) {
      if (!(key in item)) continue;
      const processPromise = processKey(key, url, item);
      promisesToWaitFor.push(processPromise);
    }
  }
  await Promise.all(promisesToWaitFor);
  console.log("loop end");
})();

EDIT:

You mentioned that you wanted to keep things in order. I decided instead of preforming all the actions side by side, we could store all the .json promises into an array, then after they have all been initiated, step through that array waiting for each one to finish. This keeps the order of things while still getting some better speed.

One thing I wanted to clarify was combining async/await with promise chaining .then method.

I have the line

const jsonPromise = fetch(`${url}/${item[key]}`).then(res => res.json());

This will use the promise chain to trigger the action of fetching and converting to json in one line. I can store that Promise to convert to json in a variable, and then store that in an array. Since the action we wanted to preform with that data involved the key used to get the data, I pass both the promise and the key to an object in the array.

After we trigger all the fetching, then I have a separate loop which uses await to wait for the action to complete.

(async () => {
  const response = await fetch(url);
  const data = await response.json();
  const fetchToWaitFor = [];
  for (const item of data.list) {
    //do something with id?
    for (const key of ["name", "address", "city"]) {
      if (!(key in item)) continue;
      const jsonPromise = fetch(`${url}/${item[key]}`).then(res => res.json());
      fetchToWaitFor.push({jsonPromise, key});
    }
  }
  for (const waitObj of fetchToWaitFor) {
    const {key, jsonPromise} = waitObj;
    const data = await jsonPromise;
    console.log(`fetch item: ${key}`);
  }
  console.log("loop end");
})();

I've suffered from this problem for a long time until I found the built-in solution: Recursive Functions.

Yep, so the theory behind this is to call a function, run the fetch, and once the fetch is completed, call the same function again. Here's an example code:

const urlsToFetch = [/* your URLs here */], result = [];
function recursive(i) {
  // Have we looped through everything in the array? If so, end the recursion
  if(i === urlsToFetch.length) {
    console.log("All done");
    console.table(result);
    return;
  }
  fetch(urlsToFetch[i]).then(function(data) {
    // Process data... do your stuff, and then at the bottom:
    result.push(someDataThatWeProcessed); // Add processed data to an array
    return recursive(i + 1); // Call this function again. This will happen until we loop through everything in the array
  });
}
recursive(0); // call the function

I've just implemented an example by calling fake api(json-server) using async-await rather than promise chain.In this example I make a loop over comments response and make 2 request on every iteration. You can replace your own url and keys in the below code and get your intended result.

async function getData() {
  console.log('====== start fetching ======')
  const commentsResponse = await fetch('https://my-json-server.typicode.com/typicode/demo/comments');
  const comments = await commentsResponse.json();
  for (comment of comments) {
    console.log('====== comment detail ======')
    for (key in comment) {
      if (key == "id") {
        console.log({commentId: comment.id});
      } else {
        if (key == "body") {
          // profile is fixed request just for test request chain is working correctly or not
          const profileResponse = await fetch('https://my-json-server.typicode.com/typicode/demo/profile');
          const profile = await profileResponse.json();
          console.log({profile})
        } else if (key == "postId") {
          const postId = comment.postId;
          const postRespnse = await fetch('https://my-json-server.typicode.com/typicode/demo/posts/' + postId);
          const post = await postRespnse.json();
          console.log({post});
        }
      }
    }
  }
}

getData();

Related