Richard Dao
Richard Dao

Reputation: 133

Firebase Firestore - Async/Await Not Waiting To Get Data Before Moving On?

I'm new to the "async/await" aspect of JS and I'm trying to learn how it works.

The error I'm getting is Line 10 of the following code. I have created a firestore database and am trying to listen for and get a certain document from the Collection 'rooms'. I am trying to get the data from the doc 'joiner' and use that data to update the innerHTML of other elements.

  // References and Variables
  const db = firebase.firestore();
  const roomRef = await db.collection('rooms');
  const remoteNameDOM = document.getElementById('remoteName');
  const chatNameDOM = document.getElementById('title');
  let remoteUser;
  // Snapshot Listener
  roomRef.onSnapshot(snapshot => {
    snapshot.docChanges().forEach(async change => {
      if (roomId != null){
        if (role == "creator"){
          const usersInfo = await roomRef.doc(roomId).collection('userInfo');
          usersInfo.doc('joiner').get().then(async (doc) => {
            remoteUser = await doc.data().joinerName;
            remoteNameDOM.innerHTML = `${remoteUser} (Other)`;
            chatNameDOM.innerHTML = `Chatting with ${remoteUser}`;
          })
        }
      }
    })
  })
})

However, I am getting the error:

Uncaught (in promise) TypeError: Cannot read property 'joinerName' of undefined

Similarly if I change the lines 10-12 to:

remoteUser = await doc.data();
remoteNameDOM.innerHTML = `${remoteUser.joinerName} (Other)`;
chatNameDOM.innerHTML = `Chatting with ${remoteUser.joinerName}`;

I get the same error.

My current understanding is that await will wait for the line/function to finish before moving forward, and so remoteUser shouldn't be null before trying to call it. I will mention that sometimes the code works fine, and the DOM elements are updated and there are no console errors.

My questions: Am I thinking about async/await calls incorrectly? Is this not how I should be getting documents from Firestore? And most importantly, why does it seem to work only sometimes?

Edit: Here are screenshots of the Firestore database as requested by @Dharmaraj. I appreciate the advice. Rooms Collection to Documents

userInfo Sub-Collection and Documents within userInfo

Upvotes: 2

Views: 3211

Answers (2)

Renaud Tarnec
Renaud Tarnec

Reputation: 83093

You are mixing the use of async/await and then(), which is not recommended. I propose below a solution based on Promise.all() which helps understanding the different arrays that are involved in the code. You can adapt it with async/await and a for-of loop as @Dharmaraj proposed.

roomRef.onSnapshot((snapshot) => {
    // snapshot.docChanges() Returns an array of the documents changes since the last snapshot.
    // you may check the type of the change. I guess you maybe don’t want to treat deletions

    const promises = [];
    snapshot.docChanges().forEach(docChange => {
        // No need to use a roomId, you get the doc via docChange.doc
        // see https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentChange

        if (role == "creator") {  // It is not clear from where you get the value of role...
            const joinerRef = docChange.doc.collection('userInfo').doc('joiner');
            promises.push(joinerRef.get());
        }

    });

    Promise.all(promises)
        .then(docSnapshotArray => {
            // docSnapshotArray is an Array of all the docSnapshots
            // corresponding to all the joiner docs corresponding to all 
            // the rooms that changed when the listener was triggered
            docSnapshotArray.forEach(docSnapshot => {
                remoteUser = docSnapshot.data().joinerName;
                remoteNameDOM.innerHTML = `${remoteUser} (Other)`;
                chatNameDOM.innerHTML = `Chatting with ${remoteUser}`;
            })
        });

});

However, what is not clear to me is how you differentiate the different elements of the "first" snapshot (i.e. roomRef.onSnapshot((snapshot) => {...}))). If several rooms change, the snapshot.docChanges() Array will contain several changes and, at the end, you will overwrite the remoteNameDOM and chatNameDOM elements in the last loop.

Or you know upfront that this "first" snapshot will ALWAYS contain a single doc (because of the architecture of your app) and then you could simplify the code by just treating the first and unique element as follows:

roomRef.onSnapshot((snapshot) => {
    const roomDoc = snapshot.docChanges()[0];
    // ...

});

Upvotes: 1

Dharmaraj
Dharmaraj

Reputation: 50840

There are few mistakes in this:

  1. db.collection() does not return a promise and hence await is not necessary there
  2. forEach ignores promises so you can't actually use await inside of forEach. for-of is preferred in that case.

Please try the following code:

const db = firebase.firestore();
const roomRef = db.collection('rooms');
const remoteNameDOM = document.getElementById('remoteName');
const chatNameDOM = document.getElementById('title');
let remoteUser;
// Snapshot Listener
roomRef.onSnapshot(async (snapshot) => {
  for (const change of snapshot.docChanges()) {
    if (roomId != null){
      if (role == "creator"){
        const usersInfo = roomRef.doc(roomId).collection('userInfo').doc("joiner");
        usersInfo.doc('joiner').get().then(async (doc) => {
          remoteUser = doc.data().joinerName;
          remoteNameDOM.innerHTML = `${remoteUser} (Other)`;
          chatNameDOM.innerHTML = `Chatting with ${remoteUser}`;
        })
      }
    }
  }
})

Upvotes: 1

Related Questions