5

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.

Lidor shimoni
  • 93
  • 1
  • 9
Koala7
  • 1,340
  • 7
  • 41
  • 83
  • Have you tried `Promise.all`? – goto Sep 08 '20 at 11:20
  • Yes, no difference – Koala7 Sep 08 '20 at 11:23
  • Now that I am looking at it closely it you're not really returning anything from the `fetchData` promise but you're doing some sort of `dispatch`, which is unclear where that's coming from, so I guess the issue you're seeing is that these dispatches are not triggered in order (which they shouldn't unless you get lucky), but if you were to call `getAllData` which returns a `Promise.all` where `fetchData` returns the final result inside `then` (instead of calling dispatch), then you'd get the right behavior. – goto Sep 08 '20 at 11:26
  • Do you mind to write the answer with a code example to better understand? – Koala7 Sep 08 '20 at 11:31
  • 1
    It's unclear how you're using the `getTeamsStats` function inside your component. – goto Sep 08 '20 at 11:34
  • I will provide a codesanbox – Koala7 Sep 08 '20 at 11:40
  • I have created a sample here https://codesandbox.io/s/bitter-water-8kc6t?file=/src/index.js using `Promise.allSettled`. It is returned in the same order as they are. My suggestion here would be to wait for `Promise.allSettled` to resolve with right status and then dispatch data accordingly. – Pranay Tripathi Sep 08 '20 at 11:41

5 Answers5

5

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.

zhuber
  • 5,364
  • 3
  • 30
  • 63
  • Based on the `dispatch` call it looks like you're using redux to manage state here. If you still want the dispatches to happen in real time you could either key the results (allowing you to select the results in the order you want) or pass an index from your initial map to allow for sorting afterwards: `return Promise.all(dates.map((x, index) => url + '/' + 357 + '/' + 5 + '/' + x).map(fetchData, index));` – aaronrhodes Sep 14 '20 at 10:07
2

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>
Seralahthan
  • 182
  • 11
1

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.

sidthesloth
  • 1,399
  • 1
  • 11
  • 19
1
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"];

    return Promise.all(
      DATES.map(date => axios.get(`${URL}/357/5/${date}`))
        .then(responseList => {
          responseList.map((res) => {
            const {
              matchsPlayed: { total: teamsTotalMatchsPlayed },
            } = res.data.api.statistics.matchs;

            const matchsPlayed = teamsTotalMatchsPlayed;

            dispatch(receivedTeamsStat(matchsPlayed, type));
          });
        })
        .catch((e) => console.log(e))
    );
  };
}
Akash Singh
  • 547
  • 5
  • 8
0

Promise.all preserves's the order. You should wait for all the api promise's to resolve first, before action dipatching.

Helpful Article: Promise.all: Order of resolved values

Dolly
  • 2,213
  • 16
  • 38