Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Promise asynchronous tasks order not correct

I am making multiple calls with Promise.

My API endpoints to fetch are:

  1. https://www.api-football.com/demo/v2/statistics/357/5/2019-08-30
  2. https://www.api-football.com/demo/v2/statistics/357/5/2019-09-30
  3. https://www.api-football.com/demo/v2/statistics/357/5/2019-10-30

See the code

export function getTeamsStats(league, team, type) {
  return function(dispatch) {

    const url = "https://www.api-football.com/demo/v2/statistics";
    let dates = ["2019-08-30", "2019-09-30", "2019-10-30"];
    const getAllData = (dates, i) => {
      return Promise.allSettled(dates.map(x => url + '/' + 357 + '/' + 5 + '/' + x).map(fetchData));
    }

    const fetchData = (URL) => {
      return axios
        .get(URL)
        .then(res => {
          const {
            matchsPlayed: { total: teamsTotalMatchsPlayed},
          } = res.data.api.statistics.matchs;

          const matchsPlayed = teamsTotalMatchsPlayed;

          dispatch(receivedTeamsStat(matchsPlayed, type));
        })
        .catch(e => {
          console.log(e);
        });
    }

    getAllData(dates).then(resp=>{console.log(resp)}).catch(e=>{console.log(e)})

  }
}

Then in my component, I put in an array the matches played from this specific team ( Sao Paulo in this example ) from the initial date 30-8-2019 to 30-10-2019

    const [dataHomeTeam, setDataHomeTeam] = useState([]);

      useEffect(() => {
    
         if (!team.matchsPlayed) {
           return ;
         }
    
         setDataHomeTeam(prev =>
           prev.concat([
             {
               matches: team.matchsPlayed,
             }
           ])
         );
    
       },[team.matchsPlayed]);

console.log('Data Array', dataHomeTeam);

The problem is that normally the in the first render of the page I have the right order of the matches made from 30-8-2019 to 30-10-2019

See the console log image

Correct order

But sometimes not, see here

enter image description here

So the question is, how can I make sure that the Promise is returning me the right order of the requests?

I am using Promise.allSettled, the multiple asynchronous tasks that are not dependent on one another to complete successfully, but the order is not always right.

like image 563
Koala7 Avatar asked May 26 '20 21:05

Koala7


3 Answers

This should be resolved via Promise.all because as described in documentation

Returned values will be in order of the Promises passed, regardless of completion order.

The problem you have is that you are setting your state by calling dispatch for each promise based on it's completion and since promises can finish at any given time (e.g. 3rd request could finish first because they are sent at the same time), dispatch will be called and your state management will set that as first response (that's why you "sometimes" get such behaviour, because it's up to the network which one will finish first).

This would mean that either you should change your dispatch to receive array of already finished promises, or call dispatch one-by-one once they are finished which is shown in code below - resolve all and dispatch in order:

export function getTeamsStats(league, team, type) {
    return function (dispatch) {
    const url = "https://www.api-football.com/demo/v2/statistics";
    let dates = ["2019-08-30", "2019-09-30", "2019-10-30"];
    const getAllData = (dates, i) => {
        return Promise.all(dates.map(x => url + '/' + 357 + '/' + 5 + '/' + x).map(fetchData));
    }

    const fetchData = (URL) => {
        return axios
            .get(URL)
            .then(res => {
                const {
                    matchsPlayed: { total: teamsTotalMatchsPlayed },
                } = res.data.api.statistics.matchs;

                return teamsTotalMatchsPlayed;
            })
            .catch(e => {
                console.log(e);
            });
    }

    getAllData(dates).then(resp => {
        console.log(resp)

        // 'resp' here are all 'teamsTotalMatchsPlayed' in correct order (here I mean order of call, not promise completion)
        // so just dispatch them in order
        resp.map(matchsPlayed => receivedTeamsStat(matchsPlayed, type));            
    }).catch(e => { 
        console.log(e) 
    })

   }
}

Please note that I've maybe made some syntax error, but you get the idea.

like image 172
zhuber Avatar answered Oct 18 '22 22:10

zhuber


The ideal way of achieving your requirment is by using Promise.all(). It is because of two main reasons,

  • To maintain the return value in the order of the Promises passed, regardless of completion order.

Returned values will be in order of the Promises passed, regardless of completion order.

Refer to the Return value section in the link

  • To reject the returned promise (short circuit) if any of the promises in the iterable are rejected.

This is also important as well. We don't need to wait for all the asynchronous iterable promises to be resolved/rejected, if the first iterable promise of fetchData rejects we can short circuit and reject the returned promise.

On the other hand Promise.allSettled(),

Promise.allSettled() method returns a promise that resolves after all of the given promises have either fulfilled or rejected, with an array of objects that each describes the outcome of each promise.

It also doesn't maintain the order in the returned promise.

Promise.allSettled() method never short-circuits. Always Promise fulfilled, never rejected.

Refer to the following comparison table between Promise.all() and Promise.allSettled(),

<script src="https://gist.github.com/Seralahthan/9934ba2bd185a8ccfbdd8e4b3523ea23.js"></script>
like image 2
Seralahthan Avatar answered Oct 18 '22 23:10

Seralahthan


How, you have done,

function updateUI(value) {
    console.log(value);
    // Do something to update the UI.
}

// Note that order of resolution of Promises is 2, 1, 3
const promise1 = new Promise((resolve) => setTimeout(resolve, 200, 1)).then(updateUI);
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 2)).then(updateUI);
const promise3 = new Promise((resolve) => setTimeout(resolve, 300, 3)).then(updateUI);
const promises = [promise1, promise2, promise3];

Promise.allSettled(promises).
  then((value) => console.log('Nothing to do here', value));

// Output: 2, 1, 3

// Here we update the UI as soon as the result is obtained. As a result, the UI is also updated in the
// order in which the promise was resolved.

In other words, we wait not only for the network call but we wait for both the network call and the UI update to complete for each id which is not you want.

How you should have done instead,

// Note that order of resolution of Promises is 2, 1, 3 (Same as previous)

const promise1 = new Promise((resolve) => setTimeout(resolve, 200, 1));
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 2));
const promise3 = new Promise((resolve) => setTimeout(resolve, 300, 3));
const promises = [promise1, promise2, promise3];


Promise.allSettled(promises).
  then((results) => results.forEach((result) => updateUI(result.value)));

// Output: 1, 2, 3

// Here, we wait for all the network requests to complete and then loop through the results and update the UI.
// This ensures that the result is in order.

If you don't want to wait for all Promises to resolve and want to update the UI as soon as one resolves and still maintain order, then you need to pass the position of the element in the array and then use that position to update the element in place at the given position in the array.

Hope this helps.

like image 1
sidthesloth Avatar answered Oct 18 '22 22:10

sidthesloth