Nested Fetching

4 min read
10/5/2022

TLDR;

const getUsers = new Promise((resolve, reject) => {
  fetch("http://localhost:3000/users")
    .then((res) => res.json())
    .then((userIds) => {
      Promise.all(
        userIds.map((userId) =>
          fetch(`http://localhost:3000/user/${userId}`).then((res) =>
            res.json()
          )
        )
      ).then((users) => resolve(users));
    });
});

Then simply to use the promise:

getUsers.then((usersDetails) => console.log(users));

Nested fetching

I’ve noticed that a lot of people are stuck on nested fetching for the upcoming Assignment 2 (dw, I was one of them when I did the course), so I’ve made a quick run-down on how to approach this section. You can find relevent tutorials on the course site as well.

Firstly the problem

You need a list of all the users including all the user details. Like follows:

users: [
	{
		userId: 0,
		name: 'Eric',
		DOB: '26/05/1999'
		profileImage: 'https://www.youtube.com/watch?v=eBGIQ7ZuuiU',
		age: 23
	},
	{
		userId: 1,
		name: 'Hayden',
		DOB: '26/05/2005'
		profileImage: 'https://www.youtube.com/watch?v=eBGIQ7ZuuiU',
		age: 12
	},
	{
		userId: 2,
		name: 'Soorria',
		DOB: '26/05/1912'
		profileImage: 'https://www.youtube.com/watch?v=eBGIQ7ZuuiU',
		age: 100
	},
]

However the server only has two endpoints, /users and /user/{userId}.

Where /users only gives you:

{
	userIds: [0, 1, 2]
}

and /user/0 gives only

{
  userId: 0,
  name: 'Eric',
  DOB: '26/05/1999'
  profileImage: 'https://www.youtube.com/watch?v=eBGIQ7ZuuiU',
  age: 23
}

So for each userId we need to pull all the information for that specific user to get all the information needed.

Naive approach

One method to solve this is to call a fetch to get the userIds and in the .then, call another set of fetches to pull all the data required as follows

const users = [];

fetch("http://localhost:3000/users")
  .then((res) => res.json())
  .then((userIds) => {
    for (const userId of userIds) {
      fetch(`http://localhost:3000/user/${userId}`)
        .then((res) => res.json())
        .then((user) =>
          users.push(
            user // where "user" contains all specific user data
          )
        );
    }
  });
  
console.log(users);

Output:

[]

However this would output nothing! This is because as the code runs the fetch is queued up in the event loop and moves onto the console.log(users) and thus the list users is still empty at that point.

Solution

Create a promise wrapper for the recursive fetches:

const getUsers = new Promise((resolve, reject) => {
  fetch("http://localhost:3000/users")
    .then((res) => res.json())
    .then((userIds) => {
      Promise.all(
        userIds.map((userId) =>
          fetch(`http://localhost:3000/user/${userId}`).then((res) =>
            res.json()
          )
        )
      ).then((users) => resolve(users));
    });
});

Then simply to use the promise:

getUsers.then((usersDetails) => console.log(usersDetails));

This would output what we wanted at the start.

Explanation

To avoid the issue with the naive approach where we dont wait for all the promises (fetches) to resolve, we ensure to wait for all the fetches to complete before we logging anything.

Firstly to abstract the issue we wrap everything we are going to do in a overarching promise called getUsers that should resolve us a list of the users with their details.

const getUsers = new Promise((resolve, reject) => {
    // do all the fetching and finally resolve with the list of the users with details
});

Then we call the inital fetch to get us all the userIds

const getUsers = new Promise((resolve, reject) => {
  fetch("http://localhost:3000/users")
    .then((res) => res.json())
    .then((userIds) => {
        // here we have all the userid [0, 1, 2]
    });
});

We then want for each of the userIds to fetch again for the users details. This sounds like we can make it into a list of promises!

Which means we can use Promise.all, this method waits for all the promises in the list to resolve before finally resolving itself (returning the list of all resolved promises).

The benifit of this is that it will resolve all the promises in the order given. (note: if even one promise fails, ie one of the userid's is bad, then the whole Promise.all will fail even if others are fine)

Promise.all(
  userIds.map((userId) =>
    fetch(`http://localhost:3000/user/${userId}`).then((res) => res.json())
  )
).then(users => console.log(users));

The above block of code will actually log exactly what we need in the order that the API gives it. So now that we have all the information we need, we can finally resolve the overarching promise with this simple line

resolve(users)

This effectively tells the Promise constructor to resolve with the users list so you can call .then and it will give you the list of users with all their details.