skip to Main Content

I actually consume some remote API using the FETCH API like below

document.addEventListener("DOMContentLoaded", () => {
    loadCart();
    aggregateSearch("france", ["api1", "api2"]);
});

const aggregateSearch = async (term, AvailableApis) => {
    console.log("aggregateSearch")
    await Promise.all(AvailableApis.map((api) => {
        fetchApi(api, term);
    }))
};

const fetchApi = async (api, term) => {
    console.log("fetchApi");
    const aggregateRawResponse = await fetch('aggregate.php', { method: 'POST', body: JSON.stringify({ source: api, term: term }) });
    const aggregateResponse = await aggregateRawResponse.json();

    console.log('response from API done');

    document.getElementById('myDiv' + api)?.innerHTML = aggregateResponse.formattedResponse;
}

const updateCartCount = async () => {
    console.log("update cartcount"); // this line is executed
    const fetchNbPhotos = await fetch("count.php");
    const data = await fetchNbPhotos.json();
    console.log("cart count done");
    document.getElementById('cartCounter').innerHTML = parseInt(data["nb"]); // this is executed once aggregateSearch done
}

const loadCart = () => {
    console.log("start loading cart");
    fetch('cart.php')
    .then((response) => {
        return response.text();
    })
    .then((html) => {
        console.log("loading cart is done updating cart count");
        document.getElementById('cart')?.innerHTML = html
        updateCartCount();
    });
}

Here the console output

start loading cart
aggregateSearch
fetchApi
loading cart is done updating cart count
update cartcount
cart count done
response from API done

The part that retrieves the contents of the APIs works perfectly, but the part that load my cart into cart div and updating my cartCounter div is only update once aggregateSearch has finished.

I tried to make loadCart async/await but without success

How can I make aggregateSearch non-blocking and update my cartCounter div while fetching API is pending ?

EDIT

I tried to change the aggregateSearch like below

const aggregateSearch = async (term, AvailableApis) => {
    console.log("aggregateSearch")
    await Promise.all(AvailableApis.map((api) => {
        return setTimeout(() => fetchApi(api, fulltext, []), 100);
        //return fetchApi(api, fulltext, []);
    }))
};

And my cartCounter div id updated instantly

EDIT2

Here is the reproductible example https://jsfiddle.net/3sdLweag/26/

Console output

"start loading cart"
"aggregateSearch"
"update cartcount"
"response from API done"
"cart count done"

As fetchApi has 2000 delay, loadCart and updateCartCount have 100 delay both expected output should be

"start loading cart"
"aggregateSearch"
"update cartcount"
"cart count done"
"response from API done"

2

Answers


  1. Hopefully I understood the issue you’re having.

    I find that asynchronous functions may still interfere with each other when running on the same document — especially when modifying elements. Although they’re running at the same time, each line of code still takes a certain amount of time to execute (especially console.log()). They may be running at the same time, but they’re also running on the same main thread more or less.

    I ran into this issue when trying to implement loading symbols or progress bars. My loading symbols stuttered and my progress bars went from 0%-100% without any other steps.

    The solution I found was Web Workers. Click here for a detailed guide for the Web Worker API.

    Using a worker allows you to send logic to an entirely separate thread from the main thread — freeing up the UI to be modified and interacted with without being interrupted.

    Here is some code modified from what I use to create and run a simple Worker with code from the same file:

    // The function to send to a Worker.
    // NOTE that the code in this function won't be able to access variables outside of it or the document directly. That will have to be handled in the message event of our Worker.
    function code(amount) {
     var values = [];
     for(var i = 0; i < amount; i++) {
      values.push(Math.random());
    
      // Send our progress back to the main thread so we can show the user.
      postMessage([(i+1)/amount]);
     }
    
     // Send our final values.
     postMessage(values);
    }
    
    // Create a javaScript file as an Object URL.
    var url = URL.createObjectURL(
     new Blob(
      // Place the code() function in the new JavaScript file.
      // Define the function for the Worker's onmessage event to start our function (doing this lets us send actual values and objects to the Worker and start it on demand).
      [`
       var code = ${code};
       onmessage = message => { postMessage(code(...message.data)); };
      `],
      {type: 'application/javascript'}
     )
    );
    
    // Create a worker with the script from our JavaScript file.
    var customworker = new Worker(url);
    
    // Add our Object URL to the customworker to access it later.
    customworker.url = url;
    
    // Add a function to revoke the Object URL when we terminate the worker.
    customworker.resolve = function() {
     URL.revokeObjectURL(this.url);
     this.terminate();
    };
    
    var progress = document.querySelector('progress');
    
    // Retrieve and process data whenever a message is posted using postMessage().
    customworker.addEventListener('message', function(message) {
     if(message.data.length == 1) {
      // Show the user the progress of our function using the progress of the for loop in code().
      progress.value = message.data[0];
     } else {
      // Terminate the worker using our custom function since we know the worker is done.
      // If we want to use the code in the worker again and again, you don't need to do this.
      this.resolve();
    
      // Show the user what we want to show them of the result.
      document.querySelector('div').innerHTML = `1000000 random numbers were found. The final number was ${message.data.slice(-1)}.`;
     }
    });
    
    // Run the function in our Worker sending a parameter of 1000000 to code().
    customworker.postMessage([1000000]);
    <div>Running Worker...</div>
    <progress max='1' value='0'/>

    Here is a simplified version of the code above

    function code() {
     // Your code here.
     // Use postMessage() to send data back to the document.
    }
    var url = URL.createObjectURL(new Blob([`var code = ${code}; onmessage = message => { postMessage(code(...message.data)); };`], {type: 'application/javascript'}));
    var customworker = new Worker(url);
    customworker.url = url;
    customworker.resolve = function() { URL.revokeObjectURL(this.url); this.terminate(); };
    customworker.addEventListener('message', function(message) {
     // Use message.data and manipulate the document here.
    });
    customworker.postMessage([/* Parameters go here. */]);
    

    You could use a Worker in aggragateSearch() or fetchAPI() to offload the intense requirements of the fetch() function to a new thread, then you can update the div you hope to change in the Worker’s ‘message’ eventListener.

    For instance, your fetchApi function might look something like this (untested):

    function fetchApi(api, term) {
     fetchApi.worker.postMessage([api, term]);
    }
    fetchApi.worker = new Worker(
     URL.createObjectURL(
      new Blob(
       [`var code = ${
         (api, term) => fetch('aggregate.php', { method: 'POST', body: JSON.stringify({ source: api, term: term }) })
          .then(result => result.json())
          .then(result => postMessage(result.formattedResponse))
        }; onmessage = message => { postMessage(code(...message.data)); };`],
       {type: 'application/javascript'}
      )
     )
    );
    fetchApi.worker.addEventListener('message', function(message) {
     document.getElementById('myDiv' + api).innerHTML = message.data;
    });
    

    If you can put your code in a real .js file, that might be cleaner than making a URL Object when using Workers.

    This solution may not meet your need if I misunderstood your issue, — and you would need to adapt it to your full code, — but hopefully this is useful to you!

    Login or Signup to reply.
  2. It looks like you want loadCart to finish its asychronous parts — ending with an update of the cartCounter — before the aggregateSearch fetch is initiated, as that is what your setTimeout version will likely achieve.

    Then the solution is to await the asynchronous tasks made by loadCart.

    First of all loadCart should return a promise. It needs a return at two spots in your code

    const loadCart = () => {
        console.log("start loading cart");
        return fetch('cart.php')  // <-- return the promise!
        .then((response) => {
            return response.text();
        })
        .then((html) => {
            console.log("loading cart is done updating cart count");
            document.getElementById('cart')?.innerHTML = html;
            return updateCartCount(); // <-- return the promise!
        });
    }
    

    Or, alternatively, write that function as an async function, just like you did at other places in your code:

    const loadCart = async () => { // <-- make it async
        console.log("start loading cart");
        const response = await fetch('cart.php');  // <-- and use await
        const html = await response.text();        // <-- 
        console.log("loading cart is done updating cart count");
        document.getElementById('cart')?.innerHTML = html;
        return updateCartCount(); // <-- return the promise!
    }
    

    Another point is that you don’t get a useful result from Promise.all, as you don’t pass it an array of promises. This makes the await operator on that Promise.all quite useless. It has no negative effect in your case, as you never use that result, but it would make more sense to do it in the right way, and have aggregateSearch return a promise that only resolves when the asynchronous tasks have completed:

    const aggregateSearch = async (term, AvailableApis) => {
        console.log("aggregateSearch");
        await Promise.all(AvailableApis.map((api) => {
            return fetchApi(api, term);  // <-- return the promise!
        }));
    };
    

    Finally, await the returned promises in your event handler:

    document.addEventListener("DOMContentLoaded", async () => {  // <-- make it async
        await loadCart(); // <-- await it
        await aggregateSearch("france", ["api1", "api2"]); // <-- await it
    });
    

    NB: that last await is not really needed, but it could become necessary when you decide to add more code in that event handler, which relies on the effects of agggregateSearch. So it is good practice to already put that await operator here.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search