Ad

Firestore Cross-reference Returning A Promise Object, Can't Access Document Value

Scenario

My app is built with React and Firebase. There is a feature in the app that allows users to create playlists and fill them with tracks from a catalog. A Playlist has a sub-collection called "Tracks" that stores the track ID's and some other info. When viewing a playlist, I want to loop through the tracks and cross-reference them (like a join in SQL) to the Tracks collection to populate the view.

// Looks something like this before the return...

// Attempting to write an async/await function
async function getTrackDocument(trackId) {
  const track = await firestore
    .collection("tracks")
    .doc(trackId)
    .get();
  return track;
}

useEffect(() => {
  const playlistRef = firestore.doc(`playlists/${playlistId}`);
  const playlistTracksRef = firestore.collection(
    `playlists/${playlistId}/tracks`
  );

  playlistRef.get().then(doc => {
    if (doc.exists) {
      const unsubscribe = playlistTracksRef.onSnapshot(snapshot => {
        // Snapshot of playlist tracks
        const playlistTracks = snapshot.docs.map(doc => {
          return {
            id: doc.id,
            ...doc.data(),
            // Getting the track document
            trackRef: getTrackDocument(doc.id).then(doc => {
              return doc.data();
            })
          };
        });

        setTracks(playlistTracks);
      });

      return () => unsubscribe();
    }
  });
});

Problem

The part where I am trying to cross-reference a Playlist Track ID to the Tracks collection:

trackRef: getTrackDocument(doc.id).then(doc => {
   return doc.data();
})

I was hoping that would give me the track information I needed. That part returns a promise object in the console:

trackRef: Promise
__proto__: Promise
[[PromiseStatus]]: "resolved"
[[PromiseValue]]: Object
__proto__: Object

And [[PromiseValue]] has all the values, but I can't get seem to get to where I can use the values. I can log doc.data() to the console and it shows me the values I need eventually. Is there a way to return those values in-line the way the code is written? I've tried different ways of writing async/await functions, and I also tried to return a Promise.resolve(data), always the same results.

The closest I've come to the behavior I was hoping for was by creating a function where I could grab the Track and just push it to tracks array in state, like:

// @param arrayOfTrackIDs -- just track IDs from the playlist
function getTracksThenPopulateArray(arrayOfTrackIDs) {
   arrayOfTrackIDs.forEach(trackID => {
      firestoreTracks.doc(trackID).get().then(doc => setTracks(tracks => [...tracks, track])); 
   }
}

This wasn't ideal however because I couldn't incorporate .onSnapshot() to update the view if tracks were removed or added.

Solution

Similar to user Gazihan's suggestion, I used his async/await Promise.all() and added async/await Promise.resolve() for the inner document and it works:

const unsubscribe = playlistTracksRef.onSnapshot(async snapshot => {
  const playlistTracks = await Promise.all(
    snapshot.docs.map(async doc => {
      return {
        id: doc.id,
        ...doc.data(),
        trackRef: await Promise.resolve(
          getTrackDocument(doc.id).then(doc => {
            return doc.data();
          })
        )
      };
    })
  );
  setTracks(playlistTracks);
});
Ad

Answer

map() is sync. With map() you can only get a list of Promise instances. You have to await each of them to make it a list with actual values. Promesi.all() does that.

Here's the fixed code:

        const playlistTracks = await Promise.all(snapshot.docs.map(doc => {
          return {
            id: doc.id,
            ...doc.data(),
            // Getting the track document
            trackRef: getTrackDocument(doc.id).then(doc => {
              return doc.data();
            })
          };
        }));

However, to be able to use that await before Promise.all(), you would need to be in an async function. Here's a try, should work:

      const unsubscribe = playlistTracksRef.onSnapshot(async snapshot => {
        // Snapshot of playlist tracks
        const playlistTracks = await Promise.all(snapshot.docs.map(doc => {
          return {
            id: doc.id,
            ...doc.data(),
            // Getting the track document
            trackRef: getTrackDocument(doc.id).then(doc => {
              return doc.data();
            })
          };
        }));

        setTracks(playlistTracks);
      });
Ad
source: stackoverflow.com
Ad