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);
});
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);
});
Related Questions
- → How to update data attribute on Ajax complete
- → October CMS - Radio Button Ajax Click Twice in a Row Causes Content to disappear
- → Octobercms Component Unique id (Twig & Javascript)
- → Passing a JS var from AJAX response to Twig
- → Laravel {!! Form::open() !!} doesn't work within AngularJS
- → DropzoneJS & Laravel - Output form validation errors
- → Import statement and Babel
- → Uncaught TypeError: Cannot read property '__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED' of undefined
- → React-router: Passing props to children
- → ListView.DataSource looping data for React Native
- → Can't test submit handler in React component
- → React + Flux - How to avoid global variable
- → Webpack, React & Babel, not rendering DOM