firestore get data from two tables one after the other return empty array

Viewed 264

I'm using firebase - firestore. I have courses and tasks collection. I want to get all the courses of user from courses collection and for each course to get days data from tasks collection and then save all this data in one array.

  getData = () => {
    var arr = []
    f.auth().onAuthStateChanged(async (user) => {
       db.collection("courses")
        .where("uid", "==", user.uid)
        .get()
        .then((snapshot) => {

          var a = {};
          snapshot.forEach((doc) => {
            let coursesData =  doc.data()
            let courseName = coursesData.name;
            let kita = coursesData.kita;
            a = { name: courseName, id: doc.data().code, k: kita };
            let snapshotData = await db
              .collection("tasks")
              .where("uid", "==", user.uid)
              .where("name", "==", courseName)
              .where("kita", "==", kita)
              .get();

            let numActiveCourse = 0;
            snapshotData.forEach((dc) => {
              let taskData =  dc.data()
              console.log('taskData',taskData)
              let days = taskData.days;
              if (days > 0) {
                numActiveCourse = 1;
              }
            });

            a = { ...a, numActiveCourse };
            arr.push(a);
            console.log("arr2 is", arr);
          });
        })
        .catch((e) => {
          console.log("error is courses", e);
        });
        this.setState({data:arr})

    });

  };

the problem is that the arr is always empty (I guess I have asyncornize issue) and the snapshot not await after it will finish.

2 Answers

I found solution. the issue it's because I tried to make async await into forEach and it not wait to answer. the solution is

 readCourses = async()=>{
f.auth().onAuthStateChanged(async (user) => {
  let loadedPosts = {};
  let docSnaps = await db.collection("courses").where("uid", "==", user.uid).get();
  for (let doc of docSnaps.docs){

    let courseName = doc.data().name;
    let kita = doc.data().kita
    loadedPosts[doc.id] = {
      ...doc.data(),
      k:kita,
      id:doc.data().code
    }
    const taskSnap =  await db
      .collection("tasks")
      .where("uid", "==", user.uid)
      .where("name", "==", courseName)
      .where("kita", "==", kita)
      .get()

    let numActiveCourse = 0
    for(let task of taskSnap.docs){
      let taskData = task.data()
      if(taskData.days>0){
        numActiveCourse =numActiveCourse+1
      }

    }
    loadedPosts[doc.id].numActiveCourse = numActiveCourse
  }
  console.log('loadedPosts',loadedPosts)
  this.setState({data:loadedPosts})

})

}

if you have any other solution I would like to see.

  1. It's not a good idea to mix await and then as it was in your original code. There was your first mistake. Not only you didn't wait for results of forEach, but this.setState({data:arr}) was outside of then and executed even before you've reached the forEach call.
  2. Another issue with your initial version of code is as you said - not waiting for results of forEach. But I'm not sure that you fully understand it. Because you didn't have to change your code so much (readability aside). All your had to do is:
    // change db.collection("courses")...then(...) to
    const snapshot = await db.collection("courses")... // only now onAuthStateChanged callback becomes async
    ...
    // then change forEach() to map() and wait for result
    const promises = snapshot.map(async (doc) => { ... })
    await Promise.all(promises)
    ...
    
  3. As for your new version: in each iteration of the for loop you await. That means that requests for every taskSnap will be executed one after another. That's bad. Especially on slow connections. Check out the snippet (I've simplified it to the bare minimum): getData with map completes in 1 second, version with for - in 4 seconds. (And you also removed catch from your new code - not a great idea.)

let i = 0
const get_courses = () => new Promise((resolve) => setTimeout(() => resolve(["a","b","c","d"]), 10))
const get_tasks   = () => new Promise((resolve) => setTimeout(() => resolve(++i), 1000))
const f_auth_onAuthStateChanged = fn => fn()

const getData = () => {
    const data = []
    f_auth_onAuthStateChanged(async (user) => {
      try {
        const courses = await get_courses()
        const promises = courses.map(async (course) => {
          const tasks = await get_tasks()
          data.push({ course, tasks })
        })
        await Promise.all(promises)
        console.log(data) // this.setState({ data })
        console.timeEnd("map")
      } catch(e) { console.error(e) }
  })
}
console.time("map")
getData()

const getData2 = () => {
    const data = []
    f_auth_onAuthStateChanged(async (user) => {
      try {
        const courses = await get_courses()
        for (const course of courses) {
          const tasks = await get_tasks()
          data.push({ course, tasks })
        }
        console.log(data) // this.setState({ data })
        console.timeEnd("for")
      } catch(e) { console.error(e) }
  })
}
console.time("for")
getData2()

  1. The readCourses function from your own answer doesn't return a Promise. So formally it's not async. That won't change anything, except for a small code readability improvement. Same goes for onAuthStateChanged callback from your original code.
Related