7

Let's say I have an async/await that calls an API that fetches all users.

async function getUsers() {
  const users = await Api.getAllUsers()

  return users.map(user => {
    return {
      id: user.id,
      group: 'data depends on subsequent API call',
    }
  })
}

Within the return map, I have to do another API call to get some data that should be in the same scope.

const groupByUser = Api.getGroupByUserId()

How do I accomplish this? Can I put an async/await within the existing one? Do I create an array of all users ids and somehow map through that? I'm kind of lost on where to go next and any input would be appreciated.

// getUsers() passed into componentDidMount() wrapper
Tania Rascia
  • 1,563
  • 17
  • 35

2 Answers2

11

Use an async function for your map function and then use Promise.all on that result.

async function getUsers() {
  const users = await Api.getAllUsers()

  return Promise.all(users.map(async (user) => {
    return {
      id: user.id,
      group: await Api.getGroupByUserId(user.id),
    }
  }))
}

When you map with an async function you essentially get an array of promises. That's why you need Promise.all

MinusFour
  • 13,913
  • 3
  • 30
  • 39
  • I just realized that `Promise.all` requires that all API calls return successfully, but the way I'm using it, not all will have a result. Is there an alternative for this? – Tania Rascia Aug 23 '18 at 18:33
  • @TaniaRascia as long as the promises resolve and aren't rejected you are fine. If one of your promises rejects you can add a `catch` clause to your `getGroupByUserId` call and return `undefined` or w.e. value you want to have there on the `group` property. You can also add `try/catch` to your code. – MinusFour Aug 23 '18 at 18:54
2

As long as you are using async/await you can continue to await in your getUsers function. You can reduce the list of users to an async function which will build a self executing async function to await. This function will build the new array.

// MOCK Api
const Api = {
    getAllUsers: async () => [{name: 'user1', id: 1}, {name: 'user2', id: 2}],
    getGroupByUserId: async (id) =>
        new Promise(resolve => setTimeout(() => resolve('users'), id*1000)),
};
// End MOCK Api

async function getUsers() {
  const users = await Api.getAllUsers()

  return await users.reduce((list, user) => {
    return async () => {
        $nextList = await list()
        $nextList.push({
            id: user.id,
            group: await Api.getGroupByUserId(user.id)
        })
        return $nextList
     }
  }, async () => [])()
}

(async () => {
    const allUserGroups = await getUsers();
    console.log(allUserGroups);
})();

// 3s -> [ { id: 1, group: 'users' }, { id: 2, group: 'users' } ]

Edit: On a side note this method makes sure that API requests happen in series, so it is a little easier on the API as far as load, but will be a slower. If you want all of the API requests to happen as soon as possible, the method using Promise.all and map will be a faster since it will make all of the API calls in parallel. ALL, so don't use that method for large lists.

NeoVance
  • 404
  • 2
  • 9
  • 1
    What about the function passed as first parameter to map? That needs to be marked async as well, or not? And how will map handle the async mapper function? – kristaps Aug 23 '18 at 15:59
  • 1
    inner `await` will be invalid since it's out of scope of `getUsers` function. – blaz Aug 23 '18 at 16:01
  • The arrow function is not marked as `async` so `await` in front of `Api.getGroupByUserId(user.id)` is not valid. – t.niese Aug 23 '18 at 16:01