DEV Community

Cover image for Build Infinite Scroll with Firebase and React Native ( Expo)
BYIRINGIRO Emmanuel
BYIRINGIRO Emmanuel

Posted on • Updated on • Originally published at expofire.hashnode.dev

Build Infinite Scroll with Firebase and React Native ( Expo)

According to the latest statistics by statista , 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 database and Expo .

Despite relational databases like PostgreSQL, MySQL and others. Firestore is a document database and saves data in JSON-like format.

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

/**
* 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Infinity Scroll Mockup.jpg

Here the final app

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

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.


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,
    };
  }
};


Enter fullscreen mode Exit fullscreen mode

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.

/** 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();
}, []);

Enter fullscreen mode Exit fullscreen mode

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. 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 and 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.

/*
 * 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 });
  };

Enter fullscreen mode Exit fullscreen mode

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

function debounce(func, timeout = 300){
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => { func.apply(this, args); }, timeout);
  };
}
Enter fullscreen mode Exit fullscreen mode

Here Expo snack for whole app

Further Reading

Firebase Firestore

React Native FlatList

Firestore paginate query

Discussion (0)