6

I'm fetching my user data and the map function is called several times for each user. I want to wait until all data was pushed to the array and then manipulate the data. I tried using Promise.all() but it didn't work. How can I wait for this map function to finish before continuing?

Needless to say that the array user_list_temp is empty when printed inside the Promise.all().

 const phone_list_promise_1 = await arrWithKeys.map(async (users,i) => {
      return firebase.database().ref(`/users/${users}`)
          .on('value', snapshot => {
              user_list_temp.push(snapshot.val());
              console.log(snapshot.val());
            })
        }
  );

Promise.all(phone_list_promise_1).then( () => console.log(user_list_temp) )

I changed the code to this but I still get a wrong output

Promise.all(arrWithKeys.map(async (users,i) => {
      const eventRef = db.ref(`users/${users}`);
      await eventRef.on('value', snapshot => {
        const value = snapshot.val();
        console.log(value);
        phone_user_list[0][users].name = value.name;
        phone_user_list[0][users].photo = value.photo;
      })
      console.log(phone_user_list[0]); 
      user_list_temp.push(phone_user_list[0]);
    }

  ));
    console.log(user_list_temp); //empty  array
}
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Soragim
  • 317
  • 7
  • 19

2 Answers2

2

It is possible to use async/await with firebase

This is how I usually make a Promise.all

const db = firebase.database();
let user_list_temp = await Promise.all(arrWithKeys.map(async (users,i) => {
    const eventRef = db.ref(`users/${users}`);
    const snapshot = await eventref.once('value');
    const value = snapshot.value();
    return value;
  })
);

This article gives a fairly good explanation of using Promise.all with async/await https://www.taniarascia.com/promise-all-with-async-await/

Here is how I would refactor your new code snippet so that you are not mixing promises and async/await

let user_list_temp = await Promise.all(arrWithKeys.map(async (users,i) => {
  const eventRef = db.ref(`users/${users}`);
  const snapshot=  await eventRef.once('value');
  const value = snapshot.val();
  console.log(value);
  phone_user_list[0][users].name = value.name;    // should this be hardcoded as 0?
  phone_user_list[0][users].photo = value.photo;  // should this be hardcoded as 0?
  console.log(phone_user_list[0]); 
  return phone_user_list[0];        // should this be hardcoded as 0?
  })
);
console.log(user_list_temp); 

Here is a simple example that uses fetch, instead of firebase:

async componentDidMount () {
  let urls = [
    'https://jsonplaceholder.typicode.com/todos/1',
    'https://jsonplaceholder.typicode.com/todos/2',
    'https://jsonplaceholder.typicode.com/todos/3',
    'https://jsonplaceholder.typicode.com/todos/4'
  ];
  let results = await Promise.all(urls.map(async url => {
    const response = await fetch(url);
    const json = await response.json();
    return json;
  }));
  alert(JSON.stringify(results))
}
Andrew
  • 26,706
  • 9
  • 85
  • 101
  • must have been the late hour. I tested it again and it didn't work like I thought it did... I've updated the post. Do you happen to know why I still get an empty array? – Soragim Feb 04 '19 at 22:34
  • You’re mixing `promises` and `async/await` you shouldn’t do both inside a `Promise.all` – Andrew Feb 04 '19 at 22:39
  • I also missed out an `await` in front of the `Promise.all` – Andrew Feb 04 '19 at 22:52
  • I tried the new firebase option and that didn't work. I changed the .on to .once and it worked as I wanted. Do you know why? – Soragim Feb 05 '19 at 18:14
  • I’m not that familiar with firebase. I just used your code and created the `Promise.all` around it. I’m glad that it is working now. – Andrew Feb 05 '19 at 18:22
  • 1
    Using once() gets the value from the database once, while using on() continues to listen for changes to the data until you call off(). From a comment in this post https://stackoverflow.com/questions/47572171/difference-between-firebase-onvalue-and-oncevalue?noredirect=1&lq=1. The comments author is an engineer at google who works on firebase – Andrew Feb 05 '19 at 18:24
  • @Andrew You're using `.map` like a `.forEach` which is kinda wrong. Just return the value from the callback and it will get included in the map result. Plus there's no benefit in mapping using an `async` callback. Just accumulate the promises and pass them to `Promise.all` to `await` them. – nicholaswmin Feb 05 '19 at 18:33
1

If I understand your question correctly, you might consider revising your code to use a regular for..of loop, with a nested promise per user that resolves when the snapshot/value for that user is available as shown:

const user_list_temp = [];

/*
Use regular for..of loop to iterate values
*/
for(const user of arrWithKeys) {

    /*
    Use await on a new promise object for this user that
    resolves with snapshot value when value recieved for
    user
    */
    const user_list_item = await (new Promise((resolve) => {

        firebase.database()
        .ref(`/users/${users}`)
        .on('value', snapshot => {

            /*
            When value recieved, resolve the promise for
            this user with val()
            */    
            resolve(snapshot.val());
        });
    }));

    /*
    Add value for this user to the resulting user_list_item
    */
    user_list_temp.push(user_list_item);
}

console.log(user_list_temp);

This code assumes that the enclosing function is defined as an asynchronous method with the async keyword, seeing that the await keyword is used in the for..of loop. Hope that helps!

Dacre Denny
  • 29,664
  • 5
  • 45
  • 65