Nested Fetching: A Deep Dive

comp6080ass2
3 min read
493 words
1 year ago

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));
        });
});
 
// How to use our promise:
getUsers.then((usersDetails) => console.log(usersDetails));

I’ve noticed that a lot of people get stuck on nested fetching for 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 relevant tutorials on the course site as well.

Understand the Challenge

You're tasked with generating a list of all users along with their detailed information. Ideally, you want an output that looks somewhat like this:

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
    }
]

But here's the twist: the server only grants access to two endpoints: /users and /user/{userId}.

While /users offers you:

{
    userIds: [0, 1, 2]
}

Accessing /user/0 serves up:

{
    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 gather all the detailed information repeatedly.

The Naive Way

One might think to fetch the userIds and then loop through each ID, making subsequent fetch calls for user details:

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));
        }
    });
  
console.log(users);

But hol up, this would be an empty array! Why? The asynchronous nature of fetch means our console.log doesn't wait for the fetches to complete. (remember you are not allowed to use async and await)

A More Refined Solution

Let's bundle our chained fetches in a promise wrapper:

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));
        });
});
 
// How to use our promise:
getUsers.then((usersDetails) => console.log(usersDetails));

eaassyyy, all done.

Behind The Magic

We avoided the issue with the naive approach by waiting for all nested fetches to resolve. Here's a brief breakdown:

  • We wrapped our operations within an overarching promise, named getUsers. This promise's sole objective? Return a list of users complete with their details.

  • We then start with fetching all the userIds inside the overarching promise.

  • With these IDs, we're ready for the next phase: fetching each user's details. This is where Promise.all is needed. It waits for the completion of all promises in the sequence, returning in the order they were given. [caution: if even one promise fails (say, a bad userId), the whole Promise.all will fail]

  • Once we've received the full details of our users, we wrap things up by resolving our main promise.