1
votes

In order to show loading progress, I'm trying to wrap my onSnapshot call in a promise upon initial fetch. Data is loading correctly, but real-time updates are not functioning correctly.

Is there a way implement this type of functionality using the onSnapshot method?

Here's my initial data grab. Real-time updates functioned correctly before implementing the promise wrapper:

const [heroesArr, setHeroesArr] = useState([]);
const db = firebase.firestore();
const dbError = firebase.firestore.FirestoreError;

useEffect(() => {
    const promise = new Promise((resolve, reject) => {
      db.collection("characterOptions")
        .orderBy("votes", "desc")
        .onSnapshot(coll => {
          const newHeroes = [];
          coll.forEach(doc => {
            const {
              name,
              votes
            } = doc.data();
            newHeroes.push({
              key: doc.id,
              name,
              votes
            });
          });
          if(dbError) {
             reject(dbError.message)
             } else {
             resolve(newHeroes);
            }
        });
    });
    promise
      .then(result => {
        setHeroesArr(result);
      })
      .catch(err => {
        alert(err);
      });
  }, [db]);

Again, data is being loaded to the DOM, but real-time updates are not functioning correctly.

2

2 Answers

7
votes

onSnapshot is not really compatible with promises. onSnapshot listeners listen indefinitely, until you remove the listener. Promises resolve once and only once when the work is done. It doesn't make sense to combine onSnapshot (which doesn't end until you say) with a promise, which resolves when the work is definitely complete.

If you want do get the contents of a query just once, just get() instead of onSnapshot. This returns a promise when all the data is available. See the documentation for more details.

0
votes

Here's what I think going on with your code: When your component mounts, your promise gets executed once via useEffect and that's where you set state. However, subsequent updates via the onSnapshot listener are not going to change the db reference, and therefore will not trigger useEffect again, and therefore will not execute the promise again, and therefore not set state again.

The only code that will execute when you receive a snapshot update is the callback function within .onSnapshot().

To fix this, I think you could try the following (I'm honestly not sure if it'll work, but worth a try):

  1. Create a variable to track the initial load: let isInitialLoad = true;
  2. Inside your promise.then(), add isInitialLoad = false;
  3. Inside your .onSnapshot(), add if (!isInitialLoad) setHeroesArr(newHeroes); – this way, on initial load setHeroesArr gets executed on the promise but on snapshot updates setHeroesAss gets executed in the .onSnapshot() callback

The downside to this approach is that setHeroesArr will be called immediately upon a snapshot change rather than being wrapped in a promise.

Hope this helps!