Well, I made it work by changing the model I was using to retrieve the data from Firebase.
I was using an outer onSnapshot with nested promises (I think I was very near here), but now I'm using nested onSnapshots and now app is behaving as expected.
So this is the new useEffect
useEffect(() => { let cancelUserPrefSuscription, cancelUserPhotoSuscription; // First onSnapshot const cancelTweetSuscription = firestore .collection('tweets') .onSnapshot((tweetSnapshot) => { const list = []; tweetSnapshot.docs.forEach((tweetDoc) => { //Second onSnapshot cancelUserPrefSuscription = firestore .collection('user_preferences') .where('uid', '==', tweetDoc.data().uid) .onSnapshot((userPrefSnapshot) => { userPrefSnapshot.docs.forEach((userPrefDoc) => { //Third onSnapshot cancelUserPhotoSuscription = firestore .collection('user_photos') .where('id', '==', tweetDoc.data().uid) .onSnapshot((userPhotoSnapshot) => { userPhotoSnapshot.docs.forEach((userPhotoDoc) => { //Taking the whole data i need from all snapshots const newData = { id: tweetDoc.id, ...tweetDoc.data(), author: userPrefDoc.data().username, authorColor: userPrefDoc.data().color, authorPhoto: userPhotoDoc.data().photoURL, }; list.push(newData); //Updating my state if (tweetSnapshot.docs.length === list.length) { setTweets(list); } }); }); }); }); }); }); return () => { cancelTweetSuscription(); cancelUserPrefSuscription(); cancelUserPhotoSuscription(); }; }, []);
Edit: Fix from comments of above code
Author: @samthecodingman
For each call to onSnapshot, you should keep track of its unsubscribe function and keep an array filled with the unsubscribe functions of any nested listeners. When an update is received, unsubscribe each nested listener, clear the array of nested unsubscribe functions and then insert each new nested listener into the array. For each onSnapshot listener attached, a single unsubscribe function should be created that cleans up the listener itself along with any nested listeners.
Note: Instead of using this approach, create a Tweet component that pulls the author's name and photo inside it.
useEffect(() => { // helper function const callIt = (unsub) => unsub(); // First onSnapshot const tweetsNestedCancelListenerCallbacks = []; const tweetsCancelListenerCallback = firestore .collection('tweets') .onSnapshot((tweetSnapshot) => { const newTweets = []; const expectedTweetCount = tweetSnapshot.docs.length; // cancel nested subscriptions tweetsNestedCancelListenerCallbacks.forEach(callIt); // clear the array, but don't lose the reference tweetsNestedCancelListenerCallbacks.length = 0; tweetsNestedCancelListenerCallbacks.push( ...tweetSnapshot.docs .map((tweetDoc) => { // (tweetDoc) => Unsubscribe const tweetId = tweetDoc.id; //Second onSnapshot const userPrefNestedCancelListenerCallbacks = []; const userPrefCancelListenerCallback = firestore .collection('user_preferences') .where('uid', '==', tweetDoc.data().uid) .limitToFirst(1) .onSnapshot((userPrefSnapshot) => { const userPrefDoc = userPrefSnapshot.docs[0]; // cancel nested subscriptions userPrefNestedCancelListenerCallbacks.forEach(callIt); // clear the array, but don't lose the reference userPrefNestedCancelListenerCallbacks.length = 0; //Third onSnapshot const userPhotoCancelListenerCallback = firestore .collection('user_photos') .where('id', '==', tweetDoc.data().uid) .limitToFirst(1) .onSnapshot((userPhotoSnapshot) => { const userPhotoDoc = userPhotoSnapshot.docs[0]; // Taking the whole data I need from all snapshots const newData = { id: tweetId, ...tweetDoc.data(), author: userPrefDoc.data().username, authorColor: userPrefDoc.data().color, authorPhoto: userPhotoDoc.data().photoURL, }; const existingTweetObject = tweets.find(t => t.id === tweetId); if (existingTweetObject) { // merge in changes to existing tweet Object.assign(existingTweetObject, newData); if (expectedTweetCount === newTweets.length) { setTweets([...newTweets]); // force rerender with new info } } else { // fresh tweet tweets.push(newData); if (expectedTweetCount === newTweets.length) { setTweets(newTweets); // trigger initial render } } }); userPrefNestedCancelListenerCallbacks.push(userPhotoCancelListenerCallback); }); // return an Unsubscribe callback for this listener and its nested listeners. return () => { userPrefCancelListenerCallback(); userPrefNestedCancelListenerCallbacks.forEach(callIt); } }) ); }); // return an Unsubscribe callback for this listener and its nested listeners. return () => { tweetsCancelListenerCallback(); tweetsNestedCancelListenerCallbacks.forEach(callIt); }; }, []);
Edit: Splitting the code in two components
Note: Changed limitToFirst(1) --> limit(1). Splitting the fetch logic in two components simplified the onSnapshot approach!
1.The Parent Component
useEffect(() => { const tweetsUnsubscribeCallback = firestore .collection('tweets') .onSnapshot((tweetSnapshot) => { const mappedtweets = tweetSnapshot.docs.map((tweetDoc) => { return { id: tweetDoc.id, ...tweetDoc.data(), }; }); setTweets(mappedtweets); }); return () => tweetsUnsubscribeCallback(); }, []);
2.The Child Component: Tweet
useEffect(() => { // Helper Function const unSubscribe = (unsub) => unsub(); //------------getting the AUTHOR USER PREFERENCE const userPrefNestedUnsubscribeCallbacks = []; const userPrefUnsubscribeCallback = firestore .collection('user_preferences') .where('uid', '==', tweet.uid) .limit(1) .onSnapshot((userPrefSnapshot) => { userPrefNestedUnsubscribeCallbacks.forEach(unSubscribe); // cancel nested subscriptions userPrefNestedUnsubscribeCallbacks.length = 0; // clear the array, but don't lose the reference //------------getting the AUTHOR PHOTO const userPhotoUnsubscribeCallback = firestore .collection('user_photos') .where('id', '==', tweet.uid) .limit(1) .onSnapshot((userPhotoSnapshot) => { // Taking the whole data I need from all snapshots setAuthor({ author: userPrefSnapshot.docs[0].data().username, authorColor: userPrefSnapshot.docs[0].data().color, authorPhoto: userPhotoSnapshot.docs[0].data().photoURL, }); }); userPrefNestedUnsubscribeCallbacks.push(userPhotoUnsubscribeCallback); }); return () => { userPrefUnsubscribeCallback(); userPrefNestedUnsubscribeCallbacks.forEach(unSubscribe); }; }, []);