1

I am trying to pass the result of the queried documents (which is an array) outside of the function. However, accessing this array outside of the onSnapshot function yields undefined.

I have spent a whole day on this trying various things but can't figure it out.

export const getComments = (taskId) => {
    const comments = [] // i want to return this array once it is filled wit documents

    const dbRef = db.collection('tasks').doc(taskId).collection('comments') // comments is a subcollection of each task document
    dbRef.onSnapshot(
        (querySnapshot) => {
            querySnapshot.forEach((doc) => {
                comments.push(doc.data()) //each doc.data() is an object that gets pushed to the comments array. Lets say that the 1st doc.data() is {userId: 'xyz', user: 'Kiu', comment: 'this is my 1st stack overflow question'}
            })
            console.log('value for comments[0] inside onSnapshot')
            console.log(comments[0]) //sucessfully display the 1st object in the array as {userId: 'xyz', user: 'Kiu', comment: 'this is my 1st stack overflow question'}
        },
        (error) => {
            throw new Error(error.message)
        }
    )
    console.log('value for comments[0] outside onSnapshot')
    console.log(comments[0]) // this is undefined for some reason????

    console.log('value for the whole comments array outside onSnapshot')
    console.log(comments) // this does actually show the whole array, however any method used here (i.e. forEach(), map() ) returns undefined. ????

    return comments
}

When calling getComments(taskId) in App.js the console.log() result is not what I expect. Can somebody please help this newbie?

--update#1 : I read elsewhere on stack overflow that onSnapshot is to activate a listener. Listeners are not to be used in an asynchronous fashion , as listeners stay 'on' until unsubscribed. So i don't think using async and await will work for this one. I think this is where I read it. See the bottom of this discussion: What is the right way to cancel all async/await tasks within an useEffect hook to prevent memory leaks in react?

Kiu
  • 9
  • 3
  • 1
    Does this answer your question? [How do I return the response from an asynchronous call?](https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – Titus Nov 19 '20 at 06:13
  • I don't think this is an issue with doing asynchronous calls(?). I read elsewhere to not use ```onSnapshot() ``` with ```await```: https://stackoverflow.com/questions/64521755/firestore-onsnapshot-or-async-await-issue-or-both and https://stackoverflow.com/questions/58562474/what-is-the-right-way-to-cancel-all-async-await-tasks-within-an-useeffect-hook-t – Kiu Nov 19 '20 at 07:07
  • 1
    But what is your purpose of the function? Do you want to create listener or just retrieve data from Firestore? – vitooh Nov 19 '20 at 09:29
  • @vitooh I would like to return an array of objects (which each contains info of a comment) to a react component for rendering a list of the comments. I am using onSnapshot so that other authorized users can view the comments when even a new one is added. – Kiu Nov 20 '20 at 00:18

2 Answers2

1

I think your problem is that you are returning (and accessing) the comments array outside of the onSnapshot() call.

According to the docs for listening to collections, you should be listening to/modifying/returning/etc. the comments array from within the onSnapshot() block:

export const getComments = (taskId) => {
  const comments = [];

  const dbRef = db.collection("tasks").doc(taskId).collection("comments");
  dbRef.onSnapshot(
    (querySnapshot) => {
      querySnapshot.forEach((doc) => {
        comments.push(doc.data());
      });
      console.log("value for comments[0] inside onSnapshot");
      console.log(comments[0]);
      return comments; // or comments.map(), comments.forEach(), etc.
    },
    (error) => {
      throw new Error(error.message);
    }
  );
};

Your useEffect hook in App.js should follow the unsubscribe pattern as described in the React docs for Effects with Cleanup.

kamillamagna
  • 216
  • 1
  • 6
  • I read multiple times the firebase docs that you linked to. I just didn't see an explicit explanation that I can't passed the ```onSnapshot()``` results. This is likely my current shallow understanding of js and listeners. ALSO yes, I will proceed to use the useEffect hook with cleanup with onSnapshot. I read a much of blogs on this yesterday, and it seems that this is the way to go to pass on the onSnapshot results. – Kiu Nov 20 '20 at 18:12
  • Your only problem was that you were handling `comments` from outside the scope of the `onSnapshot()`. `comments` (or whatever you're doing with them) need to be returned from *inside* the function passed to `onSnapshot()`. – kamillamagna Nov 20 '20 at 22:04
0

After reading a bunch of forums, and the response above from @kamillamagna, I proceeded to use useState and useEffect in conjunction with onSnapshot. The typical way of doing this seems to use onSnapshot() with useState and useEffect in the component, OR to use it in a custom hook. I rewrote my code to the following and it works as expected:

import {useState, useEffect} from 'react'
import firebase from 'firebase/app'

/*
useComments is a custom React Hook.

Below is taken from the react website: https://reactjs.org/docs/hooks-custom.html
To share stateful logic between two or more JavaScript functions, we extract it to a third function. Both components and Hooks are functions, so this works for them too!
A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.
*/

// -- returns an array of comments for a given taskId
// -- using the prefix of 'use' in front of the function name is important to identify it as a custom hook
export default function useComments(taskId) {
    const [comments, setComments] = useState([])

    useEffect(() => {
        const commentsRef = firebase
            .firestore()
            .collection('tasks')
            .doc(taskId)
            .collection('comments') // comments is a subcollection of each task document
        const unsubscribe = commentsRef.onSnapshot(
            (snapshot) => {
                const commentsArray = snapshot.docs.map((doc) => ({
                    id: doc.id,
                    ...doc.data(),
                }))
                setComments(commentsArray)
            },
            (error) => {
                throw new Error('Error: ' + error.message)
            }
        )
        return () => unsubscribe()
        // -- the return function activates when the componentWillUnmount, which is to clean up (unsubscribe) to the onSnapshot listener
    }, [])

    return comments // -- an array of comment objects with all the key value pairs, including the doc id. The doc.id will be used as the element's unique key when rendering the React component
}

I have been focusing on this issue in the React and Firestore ecosystem. I am not sure how to achieve the same effect if i was not using React (i.e. plain vanilla js). Maybe returning the snapshot result inside the next() function inside onSnapshot() is sufficient (as @kamillamagna suggested)

Kiu
  • 9
  • 3