React Native - Firebase onSnaphot + FlatList pagination

Viewed 1373

INTRODUCTION

I have a list of items with the prop "date" stored on my FireStore. In the client code, I have a FlatList with all of those items ordered by "date" (the first element is the most recent item, the second one, the item I uploaded before the element which appears first, ...)

The problem is that I only get 5 items (but it is because I don't want to get 100 items at once), and I don't know how to combine this with the FlatList's onEndReached (as it is a listener agent that has to be detached when component unmounts) to get more items following the same order.

Any ideas how to make this work? I have commented "<------------" on the lines of the code that I might have to change.

FIRESTORE DATABASE

Items -> user.uid -> userItems:

   {
     ...
     date: 1/1/1970
   },
   {
     ...
     date: 2/1/1970
   },
   ...
   {
     ...
     date: 31/1/1970 
   }

HOW MY FLATLIST HAS TO BE RENDERED:

FlatList items in order:

{ // The most recent one appears at the top of the list
  ...
  date: 31/1/1970
},
...
{
  ...
  date: 2/1/1970
},
{
  ...
  date: 1/1/1970
},

CODE

const [startItem, setStartItem] = useState(null);

useEffect(() => {
    const { firebase } = props;

    let itemsArray = [];

    // Realtime database listener
    const unsuscribe = firebase // <------- With this I get the 5 most recent items when component mounts, or only one if the user has uploaded it after the component mounts
      .getDatabase()
      .collection("items")
      .doc(firebase.getCurrentUser().uid)
      .collection("userItems")
      .orderBy("date") // Sorted by upload date  <------------------
      .startAfter(startItem && startItem.date) // <-----------------------
      .limitToLast(5) // To avoid getting all items at once, we limit the fetch to 5 items <----------
      .onSnapshot((snapshot) => {
        let changes = snapshot.docChanges();

        changes.forEach((change) => {
          if (change.type === "added") {
            // Get the new item
            const newItem = change.doc.data();

            // Add the new item to the items list
            itemsArray.unshift(newItem);
          }
        });

        // Reversed order so that the last item is at the top of the list
        setItems([...itemsArray]); // Shallow copy of the existing array -> Re-render when new items added
        setIsLoading(false);

        // Change the start item
        setStartItem(itemsArray[itemsArray.length - 1]);
      });

    return () => {
      // Detach the listening agent
      unsuscribe();
    };
  }, []);

...
 
<CardList data={items} isLoading={isLoading} onEndReached={/*how to call the function 'unsuscribe'? */} /> // <----------

What I need is to get the other next 5 more recent items when the end of the list is reached, and then, add them to the bottom of the list


UPDATE (My best approach for now)

  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [start, setStart] = useState(null);

  const limitItems = 5;
  
  const getItems = () => {
    /*
      This function gets the initial amount of items and returns a
      real time database listener (useful when a new item is uploaded)
    */

    const { firebase } = props;

    // Return the realtime database listener
    return firebase
      .getDatabase()
      .collection("items")
      .doc(firebase.getCurrentUser().uid)
      .collection("userItems")
      .orderBy("date") // Sorted by upload date
      .startAt(start)
      .limitToLast(limitItems)
      .onSnapshot((snapshot) => {
        let changes = snapshot.docChanges();

        let itemsArray = [...items];  // <------- Think the error is here

        console.log(`Actual items length: ${itemsArray.length}`); // <-- Always 0 WHY?
        console.log(`Fetched items: ${changes.length}`); // 5 the first time, 1 when a new item is uploaded

        changes.forEach((change) => {
          if (change.type === "added") {
            // Get the new fetched item
            const newItem = change.doc.data();

            // Add the new fetched item to the head of the items list
            itemsArray.unshift(newItem);
          }
        });

        // The last item is at the top of the list
        setItems([...itemsArray]); // Shallow copy of the existing array -> Re-render when new items added

        // Stop loading
        setIsLoading(false);

        // If this is the first fetch...
        if (!start && itemsArray.length) {
          // Save the startAt snapshot
          setStart(itemsArray[itemsArray.length - 1].date);
        }
      });
  };


  const getMoreItems = () => {
    /*
      This funciton gets the next amount of items 
      and is executed when the end of the FlatList is reached 
   */

    const { firebase } = props;

    // Start loading
    setIsLoading(true);

    firebase
      .getDatabase()
      .collection("items")
      .doc(firebase.getCurrentUser().uid)
      .collection("userItems")
      .orderBy("date", "desc")
      .startAfter(start)
      .limit(limitItems)
      .get()
      .then((snapshot) => {
        let itemsArray = [...items];

        snapshot.forEach((doc) => {
          // Get the new fethed item
          const newItem = doc.data();

          // Push the new fetched item to tail of the items array
          itemsArray.push(newItem);
        });

        // The new fetched items will be at the bottom of the list
        setItems([...itemsArray]); // Shallow copy of the existing array -> Re-render when new items added

        // Stop loading
        setIsLoading(false);

        // Save the startAt snapshot everytime this method is executed
        setStart(itemsArray[itemsArray.length - 1].date);
      });
  };


  useEffect(() => {
    // Get a initial amount of items and create a real time database listener
    const unsuscribe = getItems();

    return () => {
      // Detach the listening agent
      unsuscribe();
    };
  }, []);

With this code I can fetch an initial amount of items the first time, and then the next amount when I reach the end of my FlatList. But for some reason the state is not updated inside the listener... so when a new item is uploaded, all the items I got before disapears from the FlatList and they are fethed again when the end of the FlatList is reached.

1 Answers

Okey, after some hours coding I have found a solution. I think this is not the best because it will be better to use the onSnapshot also when the end of the FlatList is reached, but I don't know if this is possible with the Firestore's onSnapshot implementation.

The solution is based on "my best approach" code which is in the question.

Algorithm:

  1. Just, at a first time, I create the Real-time Database Listener, which just does an onSnapshot and then call my function onItemsCollectionUpdate (passing the snapshot as argument), which can perfectly access the updated state of the app (as it is not inside the listener agent)

  2. When we are in the onItemsCollectionUpdate, we just get the items from the snapshot and add them to the items state.

  3. When the end of the FlatList is reached, we just call the function "getItems", which does an static retrieve of the Firestore data (I mean, using the get method from Firebase) and add it to the items state.

  4. When component unmounts, detach the listener agent.

Related