Build Infinite Scroll with Firebase and React Native ( Expo)
Tags: reactnative, react, javascript, firebase
Published: 2021-10-17 22:15:15


According to the latest statistics by  [statista](https://www.statista.com/statistics/433871/daily-social-media-usage-worldwide/) , the average time  spent on social media is 145 minutes, or 2 hours and 25 minutes every day. Infinite scrolling  is key factor to make users stay longer on social apps  which result in increase revenue and users retention.

### What Is Infinite Scroll? 

A design technique where, as the user scrolls down a page, more content automatically and continuously loads at the bottom, eliminating the user's need to click to the next page. The idea behind infinite scroll is that it allows people to enjoy a frictionless scrolling experience.

In this tutorial we will implement this design pattern with Firebase's  [Firestore](https://firebase.google.com/docs/firestore)  database and  [Expo](http://expo.io/) . 

Despite relational databases like PostgreSQL, MySQL and others.  Firestore is a [document ](https://firebase.google.com/docs/firestore/data-model) database and saves data in JSON-like format.


Firestore **collection** contains **documents**, the same **SQL table** contain **records**. 


```json
/**
* Firestore collection which documents data structure
*/
{
  "xyrt023": {
    "id": "xyrt02",
    "fullName": "Leonard M. Adleman",
    "knownFor": "Computational Complexity Theory, Cryptography",
    "bio": "Adleman was born in San Francisco...",
    "avatar": "https://res.cloudinary.com/highereducation/image/upload/h_300,w_180,c_scale,f_auto,q_auto:eco,/v1/TheBestSchools.org/leonard-adleman"
  },
  "y7rt0bb": {
    "id": "y7rt0bb",
    "fullName": " Frances E. Allen",
    "knownFor": "Compilers, Program optimization, Parallel computing",
    "bio": "Allen was born in the town of Peru....",
    "avatar": "https://res.cloudinary.com/highereducation/image/upload/h_300,w_180,c_scale,f_auto,q_auto:eco,/v1/TheBestSchools.org/frances-allen"
  },
  "qoft080": {
    "id": "qoft080",
    "fullName": " Timothy J. Berners-Lee",
    "knownFor": "Network design, World Wide Web, HTTP",
    "bio": "Berners-Lee was born in London in ....",
    "avatar": "https://res.cloudinary.com/highereducation/image/upload/h_300,w_180,c_scale,f_auto,q_auto:eco,/v1/TheBestSchools.org/timothy-berners-lee-1"
  }
}
``` 


With that knowledge, It's time to build a simple mobile app listing the most influential computer scientists.


![Infinity Scroll Mockup.jpg](https://cdn.hashnode.com/res/hashnode/image/upload/v1632499556601/sGCOZSOoM.jpeg)

[Here the final app](https://snack.expo.dev/@expofire/infinity-scroll-with-firebase)




### Batching Stream of Content

Continuously stream content require fetching data as multiple batches with limited size. Ideally, each content batch has at least 10 items

When the app is initialized, we will fetch the initial batch includes 10 documents, and save the last document ID from the initial batch to use it as the starting point for the next batch and recursively for all next batches.

![Batching- Infinity Scroll.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1633621802882/9jV8rh5h3.png)

To make our life easier, Let write a function with the following responsibilities:
 
1. When the last document ID is not provided, it starts from the first document in the collection, otherwise starts after the last document from the previous batch.

2. For each batch, the function will return an object contains  :



 **docs** : array of documents in current batch.

**lastDocId**  : last  document ID from previous  batch to be used  as starting point for next  batch.

                             
**status** : asynchronous loading status which should be `UNDETERMINED`, `PENDING`,`SUCCEEDED` or  `FAILED`.

**error** :   returned by Firestore when something went wrong.



```js

import firebase from "firebase";

const collection = firebase.firestore().collection("[COLLECTION_NAME_HERE]");

/**
 * Utilities function to extract documents in snapshots
 */

const extractSnapshots = (snapshots) => {
  let extracts = [];
  snapshots.forEach((documentSnapshot) => {
    extracts.push(documentSnapshot.data());
  });
  return extracts;
};

/**
 * Retrieve documents in batches of specified limit.
 * when last document  ID  provided, fetch documents after that
 * document (pagination query fetching)
 * @param {String} options.lastDocId -  ID of last document in previous batch
 * @param {Number} options.limit -  limit of documents per batch
 *
 * @returns - promise which will resolve into  object contains `docs`,`lastDoc`,`status`,`error`
 *
 */

const getDocs = async ({ lastDocId, limit = 10 }) => {
  let docs = []; // Array of docs in current bath
  let newLastDocId = null; // Last document ID in this batch
  let error = null;
  let batch;

  /***
   *  Fetching  documents is asynchronous operation,  It's good practice to
   *  to monitor each status of operation. Status should be UNDETERMINED, PENDING, SUCCEEDED
   *  or FAILED.
   */
  let status = "undetermined";

  try {
    /***
     * In case lastDocId provided, start after that document, otherwise
     * start on first document.
     */

    if (lastDocId) {
      const lastDoc = await collection.doc(lastDocId).get();

      /**
       *  Read more about Firestore paginated query here
       *  https://firebase.google.com/docs/firestore/query-data/query-cursors#paginate_a_query
       */
      batch = collection
        .orderBy("createdAt", "desc")
        .startAfter(lastDoc)
        .limit(limit);
    } else {
      /**
       *  The {lastDocId} not provided. Start on first document in collection
       */
      batch = collection.orderBy("createdAt", "desc").limit(limit);
    }

    status = "pending";
    const snapshots = await batch.get();

    /**
     *  For current batch, keep lastDocId to be used in next batch
     *  as starting point.
     */

    newLastDocId =
      snapshots.docs[snapshots.docs.length - 1]?.data()?.id || null;

    docs = extractSnapshots(snapshots);
    status = "succeeded";

    return {
      status,
      error,
      docs,
      lastDocId: newLastDocId,
    };
  } catch (error) {
    status = "failed";
    return {
      status,
      error: error,
      docs,
      lastDocId: newLastDocId,
    };
  }
};


```

### Fetch Initial Batch

When app initialized or main component mounted, by using `useEffect` hook, we fetch initial batch documents and save last document ID for this batch to be used as the start point for next batch.

```js
/** Fetch initial batch docs and save last document ID */
const getInitialData = async () => {
  setData({ initialBatchStatus: "pending", error: null });
  const {
    docs,
    error,
    lastDocId,
    status: initialBatchStatus,
  } = await getDocs({ limit: 10 });

  if (error) {
    return setData({ initialBatchStatus, error });
  }
  return setData({ initialBatchStatus, docs, lastDocId });
};

useEffect(() => {
  // Load initial batch documents when main component mounted.
  getInitialData();
}, []);

``` 

### Fetch next batches 

Before we proceed with fetching the next batch, let us examine how to render the content.
We use 2 components. 

 1. `<ListItem>` : Re-usable component to render document information, in our context, it's information for each scientist.

 2. `<List>` : By using React Native built-in  [FlatList](https://reactnative.dev/docs/flatlist). It renders the list of `<ListItem/>` components.

Interesting things here are props provided by FlatList, which help us to determine how far user reach scrolling content then the app can fetch the next batch. Those props are  [ onEndReachedThreshold](https://reactnative.dev/docs/flatlist#onendreachedthreshold)   and [onEndReached](https://reactnative.dev/docs/flatlist#onendreached).

 `onEndReachThreshold` set to `0.5` which translate to the half of scrollable height, it simply means that whole scrollable height equal `1`. You can set to any value you want in range between 0 to 1.

When user scroll  until half of content, this indicate that, she has interest to view more content and FlatList fires `onEndReached` event which trigger function to fetch next batch of documents then append new fetched documents to existing ones.

```js
/*
 * Fetch next batch of documents start from {lastDocId}
 */
  const getNextData = async () => {
    // Discard next API call when there's pending request
    if (data.nextBatchStatus === "pending" || !data.lastDocId) return;

    setData({ ...data, nextBatchStatus: "pending", error: null });
    const {
      docs,
      error,
      lastDocId,
      status: nextBatchStatus,
    } = await getDocs({ limit: 3, lastDocId: data.lastDocId });

    if (error) {
      return setData({ nextBatchStatus, error });
    }

    const newDocs = [...data.docs].concat(docs);
    return setData({ ...data, nextBatchStatus, docs: newDocs, lastDocId });
  };

``` 


Fetching documents is an asynchronous operation that should take a while depending on user device network speed or server availability, the app will show the Activity Indicator component when the request is pending by listening to `nextBatchStatus` when equal to `pending`.


### Debouncing Server Calls

Debounce is fancy way to say that we want to trigger a function, but only once per use case.

Let's say that we want to show suggestions for a search query, but only after a visitor has finished typing it.

Or we want to save changes on a form, but only when the user is not actively working on those changes, as every "save" costs us a database read.

When user scroll and reach threshold we trigger new documents fetch, but when user is scrolling quickly we don't have to trigger more unnecessary requests. 

By debouncing the `getNextData` function, we can delay it for a certain period like `1000` ms and save database cost while optimizing the app for performance.

Here simple debounce function

```js
function debounce(func, timeout = 300){
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => { func.apply(this, args); }, timeout);
  };
}
``` 

[Here Expo snack for whole app](https://snack.expo.dev/@expofire/infinity-scroll-with-firebase) 




### Further Reading

 [Firebase Firestore](https://firebase.google.com/docs/firestore/) 

 [React Native FlatList](https://reactnative.dev/docs/flatlist) 

 [ Firestore paginate query](https://firebase.google.com/docs/firestore/query-data/query-cursors#paginate_a_query)