Aly
Aly

Reputation: 361

Wait until onSnapshot data is fetched Firestore Firebase Next JS

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

Upvotes: 1

Views: 1661

Answers (2)

ErnestoC
ErnestoC

Reputation: 2904

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.

Upvotes: 3

BlazeFast
BlazeFast

Reputation: 1048

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

Upvotes: 0

Related Questions