Infinite scroll in Firebase (firestore) and React.js

hadi

Abdulhadi Bakr

Posted on January 20, 2021

Infinite scroll in Firebase (firestore) and React.js

Why you should use infinite scroll in your App?

When you have a lot of data, that should be shown in any page in your app, it's not efficient and not recommended to fetch them all at once. This way will make the app slow and will provide a bad user experience.
So the solution here is to use 'Infinite Scroll' to fetch them as batches.

How does infinite scroll work in firebase?

First off, fetch first 10 documents for example, then store key of last fetched document (key could be any field in document), then use this key to execute new query to fetch next 10 documents starting after last fetched document.

In firebase you can apply pagination by using 3 methods:

  1. orderBy(): specify the sort order for your documents by using any filed in document.
  2. stratAfter(): to define the start point for a query. After any document should the next batch start?
  3. limit(): limit the number of documents retrieved.

The queries will looks like:

const firstBatch = db.collection('posts')
  .orderBy('createdAt')
  .limit(5)
  .get();

const nextBatch = db.collection('posts')
  .orderBy('createdAt')
  .startAfter(last_doc_in_firstBatch.createdAt)
  .limit(5)
  .get();
Enter fullscreen mode Exit fullscreen mode

⚠️ Note: the field that will be used in orderBy() and startAfter() should be the same field ex. 'createdAt'

Let's start coding.. 👨‍💻🏃‍♂️

Database (firestore)

Alt Text

Folders and files structure

Project structure plays an important role in project maintenance, and provides the ability to scale. so our structure will looks like:

Alt Text

services
contains the files which will execute the queries on database (fetch posts).


utils
contains utility functions that will be used repeatedly in the project (firebase reference).


firebase.js
Contains firebase config and reference to database, that will be used in Post.js to execute the queries.

import firebase from "firebase/app";
import "firebase/firestore";

const firebaseConfig = {
  apiKey: "AIzaSyBL1gveQXduGppv-llH_x_w4afHkFU_UeU",
  authDomain: "fir-38a4a.firebaseapp.com",
  projectId: "fir-38a4a",
  storageBucket: "fir-38a4a.appspot.com",
  messagingSenderId: "824375282175",
  appId: "1:824375282175:web:353e6759f7d8378fe33fca"
};

firebase.initializeApp(firebaseConfig);

const db = firebase.firestore();

export default db;
Enter fullscreen mode Exit fullscreen mode

Post.js
Contains the queries, that will fetch the posts from database.

import db from "../utils/firebase";

export default {
  /**
   * this function will be fired when you first time run the app,
   * and it will fetch first 5 posts, here I retrieve them in descending order, until the last added post appears first.
   */
  postsFirstBatch: async function () {
    try {
      const data = await db
        .collection("posts")
        .orderBy("createdAt", "desc")
        .limit(5)
        .get();

      let posts = [];
      let lastKey = "";
      data.forEach((doc) => {
        posts.push({
          postId: doc.id,
          postContent: doc.data().postContent
        });
        lastKey = doc.data().createdAt;
      });

      return { posts, lastKey };
    } catch (e) {
      console.log(e);
    }
  },

  /**
   * this function will be fired each time the user click on 'More Posts' button,
   * it receive key of last post in previous batch, then fetch next 5 posts
   * starting after last fetched post.  
   */
  postsNextBatch: async (key) => {
    try {
      const data = await db
        .collection("posts")
        .orderBy("createdAt", "desc")
        .startAfter(key)
        .limit(5)
        .get();

      let posts = [];
      let lastKey = "";
      data.forEach((doc) => {
        posts.push({
          postId: doc.id,
          postContent: doc.data().postContent
        });
        lastKey = doc.data().createdAt;
      });
      return { posts, lastKey };
    } catch (e) {
      console.log(e);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

App.js
First off, import 'Post.js' file.

import Post from "./services/Post";
Enter fullscreen mode Exit fullscreen mode

Then init local state using 'useState' hook.

  const [posts, setPosts] = useState([]);
  const [lastKey, setLastKey] = useState("");
  const [nextPosts_loading, setNextPostsLoading] = useState(false);
Enter fullscreen mode Exit fullscreen mode

Then in 'useEffect' fetch first batch of posts and lastKey, and set them in local state, when you first time run the app, first 5 posts will be shown.

  useEffect(() => {
    // first 5 posts
    Post.postsFirstBatch()
      .then((res) => {
        setPosts(res.posts);
        setLastKey(res.lastKey);
      })
      .catch((err) => {
        console.log(err);
      });
  }, []);
Enter fullscreen mode Exit fullscreen mode

Then create a function to fetch next batch of posts, this function receive 'lastKey' as argument. It will be fired when user click on 'More Posts' button.

  const fetchMorePosts = (key) => {
    if (key.length > 0) {
      setNextPostsLoading(true);
      Post.postsNextBatch(key)
        .then((res) => {
          setLastKey(res.lastKey);
          // add new posts to old posts
          setPosts(posts.concat(res.posts));
          setNextPostsLoading(false);
        })
        .catch((err) => {
          console.log(err);
          setNextPostsLoading(false);
        });
    }
  };
Enter fullscreen mode Exit fullscreen mode

Then create a constant to store all our posts

  const allPosts = (
    <div>
      {posts.map((post) => {
        return (
          <div key={post.postId}>
            <p>{post.postContent}</p>
          </div>
        );
      })}
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

Last step, UI

  return (
    <div className="App">
      <h2>Infinite scroll in Firebase(firestore) and React.js</h2>
      <div>{allPosts}</div>
      <div style={{ textAlign: "center" }}>
        {nextPosts_loading ? (
          <p>Loading..</p>
        ) : lastKey.length > 0 ? (
          <button onClick={() => fetchMorePosts(lastKey)}>More Posts</button>
        ) : (
          <span>You are up to date!</span>
        )}
      </div>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

⚠️ Note: when there are no more posts 'lastKey' will be set to '', therefore we check its length here, until we can detect that there are no more posts.

Live demo 🎊

Get the full code of this article. 📁


If you would like to check this feature in a real project, look here I applied it to home page posts in my last social network project (in react.js).


I hope that you found this article useful, and you enjoyed the article 😊

Bye 👋

💖 💪 🙅 🚩
hadi
Abdulhadi Bakr

Posted on January 20, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related