1

On Click I want to fetch onSnapshot instance of data which startAfter current data. But When the request is sent for the 1st time it returns an empty array. I have to click twice to get new Data.

export default function Page() {
    const [data, setData] = useState([])
    const [newData, setNewData] = useState([])
    const [loading, setLoading] = useState(false)
    const [postsEnd, setPostsEnd] = useState(false)
    const [postLimit, setPostLimit] = useState(25)
    const [lastVisible, setLastVisible] = useState(null)


    useEffect(() => {
        const collectionRef = collection(firestore, "Page")
        const queryResult = query(collectionRef, orderBy("CreatedOn", "desc"), limit(postLimit));
        const unsubscribe = onSnapshot(queryResult, (querySnapshot) => {
            setLastVisible(querySnapshot.docs[querySnapshot.docs.length-1])
            setData(querySnapshot.docs.map(doc => ({
                ...doc.data(),
                id: doc.id,
                CreatedOn : doc.data().CreatedOn?.toDate(),
                UpdatedOn : doc.data().UpdatedOn?.toDate()
                }))
            )
        });
        return unsubscribe;
    }, [postLimit])
    const ths = (
        <tr>
            <th>Title</th>
            <th>Created</th>
            <th>Updated</th>
        </tr>
    );
    const fetchMore = async () => {
        setLoading(true)
        const collectionRef = collection(firestore, "Page")
        const queryResult = query(collectionRef, orderBy("CreatedOn", "desc"), startAfter(lastVisible), limit(postLimit));
        onSnapshot(await queryResult, (querySnapshot) => {
            setLastVisible(querySnapshot.docs[querySnapshot.docs.length-1])
            setNewData(querySnapshot.docs.map(doc => ({
                ...doc.data(),
                id: doc.id,
                CreatedOn : doc.data().CreatedOn?.toDate(),
                UpdatedOn : doc.data().UpdatedOn?.toDate()
                }))
            )
        });
        //Commented If Statement cause data is not fetched completely
        //if (newData < postLimit ) { setPostsEnd(true)  }
        setData(data.concat(newData))
        setLoading(false)
        console.log(newData)
    }
    return (
        <>
        <Table highlightOnHover>
          <thead>{ths}</thead>
          <tbody>{
              data.length > 0
              ? data.map((element) => (
                <tr key={element.id}>
                  <td>{element.id}</td>
                  <td>{element.CreatedOn.toString()}</td>
                  <td>{element.UpdatedOn.toString()}</td>
                </tr>
            ))
              : <tr><td>Loading... / No Data to Display</td></tr>
          }</tbody>
        </Table>
        {!postsEnd && !loading ? <Button fullWidth variant="outline" onClick={fetchMore}>Load More</Button> : <></>}
        </>
    )
}


Current Result Current Result
Expected Result Expected Result

Aly
  • 321
  • 1
  • 4
  • 15
  • Are you able to see documents returned from the query by itself when your `useEffect()` initially runs? – ErnestoC Jun 06 '22 at 22:06

2 Answers2

3

The problem seems to be rooted in React updating the state asynchronously. I saw the same behavior using your code in my Next.js project. When looking at it closely, the onSnapshot() function and your query are working as expected.

You cannot await onSnapshot() (code reference) since it's not an asynchronous function, so the async/await in fetchMore() does not have any effect. Regardless, React state update is asynchronous, so setData(data.concat(newData)) might run before newData is updated. By the second time you load more documents, newData will have the document data.

It looks like fetchMore() can be done without relying on a "newData" state, since it was only used to update the actual data and to hide the "Load More" button:

  const fetchMore = () => {
    setLoading(true)
    const collectionRef = collection(firestore, "page")
    const queryResult = query(collectionRef, orderBy("CreatedOn", "desc"), startAfter(lastVisible), limit(postLimit));
    onSnapshot(queryResult, (querySnapshot) => {
      setLastVisible(querySnapshot.docs[querySnapshot.docs.length - 1]);
      //paginatedDocs simply holds the fetched documents
      const paginatedDocs = querySnapshot.docs.map(doc => ({
        ...doc.data(),
        id: doc.id,
        CreatedOn: doc.data().CreatedOn?.toDate(),
        UpdatedOn: doc.data().UpdatedOn?.toDate()
      }));
      //Replaced usage of newData here with the amount of documents in paginatedDocs
      if (paginatedDocs.length < postLimit ) { setPostsEnd(true) }
      //Updates the data used in the table directly, rebuilding the component
      setData(data.concat(paginatedDocs));
    });
    setLoading(false);
  }

With these changes, there is no problem updating the documents at the first try when clicking the button.

ErnestoC
  • 2,660
  • 1
  • 6
  • 19
0

Do you have persistence on? It could be firing once with the local result and again with the server data.

Anthony Chuinard
  • 1,028
  • 10
  • 17
  • I'm quite a noob. So I'm unable to understand what you are saying can you elaborate a litte? – Aly Jun 05 '22 at 09:21