'How to paginate Cloud Firestore data with ReactJs

I'm working with Firebase - Cloud Firestore and at the moment I would like to paginate all the records available. I already have a list of records and what is left is some pagination for this. I'm new with Cloud Firestore, so any clarity is appreciated.

I checked the Firestore documentation (https://firebase.google.com/docs/firestore/query-data/query-cursors#paginate_a_query) and examples with ReactJS, but there is not much available.

I understand that eg:.startAt(0), .limit(10), but the question is how to paginate properly with this component called at the render method.

import React, { Component } from 'react';
import Pagination from "react-js-pagination";
import firestore from "./Firebase";

export default class DataList extends Component {

constructor(props) {
    super(props);
    this.state = {
        dbItems: [],
        currentPage: 1,
        itemsPerPage: 3,
        totalItemCount: 1,
        activePage: 15
    }
    this.handlePageChange = this.handlePageChange.bind(this);
}

handlePageChange(pageNumber) {
    console.log(`active page is ${pageNumber}`);
    this.setState({ activePage: pageNumber });
}

async getItems() {
    const { currentPage, itemsPerPage } = this.state;
    const startAt = currentPage * itemsPerPage - itemsPerPage;
    const usersQuery = firestore.collection('Users').orderBy("email").startAt(startAt).limit(itemsPerPage)
    const snapshot = await usersQuery.get()
    const items = snapshot.docs.map(doc => doc.data())
    return this.setState({ 
        dbItems: items,
        totalItemCount: firestore.collection('Users').get().then(res => console.log(res.size))
    })

}

componentDidMount() {
    this.getItems()
}

componentDidUpdate(prevProps, prevState) {
    const isDifferentPage = this.state.currentPage !== prevState.currentPage
    if (isDifferentPage) this.getItems()
}

render() {
    return (
        <div>
            {this.state.dbItems.map((users, index) => {
                return (
                    <p key={index}>
                        <b>First Name:</b> {users.firstname} <br />
                        <b>Email:</b> {users.email}
                    </p>
                )
            })
            }
            <Pagination
                activePage={this.state.activePage}
                itemsCountPerPage={this.state.itemsPerPage}
                totalItemsCount={this.state.totalItemCount}
                pageRangeDisplayed={this.state.itemsPerPage}
                onChange={this.handlePageChange}
            />
        </div>
    )
}
}

Thank you for the help!



Solution 1:[1]

Pagination can be achieved using startAt()

// Get Items.
async fetchUsers = () => {

  // State.
  const {users, usersPerPage} = this.state

  // Last Visible.
  const lastVisible = users && users.docs[users.docs.length - 1]

  // Query.
  const query = firestore.collection('Users')
    .orderBy('email')
    .startAfter(lastVisible)
    .limit(usersPerPage)

  // Users.
  const users = await query.get()

  // ..
  return this.setState({users})

}

// Did Mount.
componentDidMount() {
  this.fetchUsers()
}

// Did Update.
componentDidUpdate(prevProps, prevState) {
  const isDifferentPage = this.state.currentPage !== prevState.currentPage
  if (isDifferentPage) this.fetchUsers()
}

Solution 2:[2]

Anyone new to Firestore and Firestore Pagination with ReactJS that would be kinda confusing to understand how Pagination will work or when to trigger call to next set of documents in firestore. anyone struggle like this try my example to make some ideas and process ahead.(Im using React-Bootstrap to render UI Elements)

01 - Install Package react-infinite-scroll-component

First Install this package yarn add react-infinite-scroll-component

02 - Include Package

Include it to your file by 'import InfiniteScroll from 'react-infinite-scroll-component';' importing it

03 - Init State

initiate state with empty list array

this.state = {
    list: [],
};

04 - Create Function to get first set of data and initiate it with component did mount

//component did mount will fetch first data from firestore 
componentDidMount(){
    this.getUsers()
}

getUsers(){
  let set = this
  //initiate first set
  var first = set.ref.collection("users").limit(12);
  first.get().then(function (documentSnapshots) {
    // Get the last visible document
    var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
    //initiate local list 
    const list = [];
    documentSnapshots.forEach(function(doc) {
        //im fetching only name and avatar url you can get any data 
        //from your firestore as you like
        const { name, avatar_full_url } = doc.data();
        //pushing it to local array
        list.push({ key: doc.id, name, avatar_full_url });
    });
        //set state with updated array of data 
        //also save last fetched data in state
        set.setState({ list, last: lastVisible });
    });
}

05 - Create function to get balance data set

fetchMoreData = () => {
  let set = this
  //get last state we added from getUsers()
  let last = this.state.last
  var next = set.ref.collection("users").startAfter(last).limit(12);
  next.get().then(function (documentSnapshots) {
  // Get the last visible document
  var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
  const list = [];
  documentSnapshots.forEach(function(doc) {
    //im fetching only name and avatar url you can get any data 
    //from your firestore as you like
    const { name, avatar_full_url } = doc.data();
    list.push({ key: doc.id, name, avatar_full_url });
  });
  //set state with updated array of data 
  //also save last fetched data in state
  let updated_list = set.state.list.concat(list);
  set.setState({ list: updated_list, last: lastVisible });
  });
};

06 - Render UI

<InfiniteScroll 
  dataLength={this.state.list.length}
  next={this.fetchMoreData}
  hasMore={true}
  loader={<span className="text-secondary">loading</span>}>
    <Row className="mt-3">
      { this.state.list.map((single, index) => (
      <Col lg={4} key={ index }>
        <div>
          <Image src={ single.avatar_full_url }roundedCircle width="100" />
          <h2>{ single.name }</h2>
        </div>
      </Col>
      ))}
    </Row>  
</InfiniteScroll>

Solution 3:[3]

Use startAt() or startAfter() for that

firestore
 .collection("Users")
 .startAt(0)
 .limit(10)
 .get()

Solution 4:[4]

Check this example this could help anyone who trying previous / next pagination

//initial state
const [list, setList] =  useState([]);
const [page, setPage] =  useState(1);

//loading initial data
useEffect(() => {
    const fetchData = async () => {
        await firebase.firestore().collection('users')
            .orderBy('created', 'desc') //order using firestore timestamp
            .limit(5) //change limit value as your need
            .onSnapshot(function(querySnapshot) { 
                var items = [];
                querySnapshot.forEach(function(doc) {
                    items.push({ key: doc.id, ...doc.data() });
                });
                setList(items);
            })
    };
    fetchData();
}, []);

After loading initial data use following function for next button trigger

//next button function
    const showNext = ({ item }) => {
        if(list.length === 0) {
            //use this to show hide buttons if there is no records
        } else {
            const fetchNextData = async () => {
                await firebase.firestore().collection('users')
                    .orderBy('created', 'desc') //order using firestore timestamp
                    .limit(5) //change limit value as your need
                    .startAfter(item.created) //we pass props item's first created timestamp to do start after you can change as per your wish
                    .onSnapshot(function(querySnapshot) {
                        const items = [];
                        querySnapshot.forEach(function(doc) {
                            items.push({ key: doc.id, ...doc.data() });
                        });
                        setList(items);
                        setPage(page + 1) //in case you like to show current page number you can use this
                    })
            };
            fetchNextData();
        }
    };
    

Then Previous button function

//previous button function
const showPrevious = ({item}) => {
    const fetchPreviousData = async () => {
        await firebase.firestore().collection('users')
            .orderBy('created', 'desc')
            .endBefore(item.created) //this is important when we go back
            .limitToLast(5) //this is important when we go back
            .onSnapshot(function(querySnapshot) {
                const items = [];
                querySnapshot.forEach(function(doc) {
                    items.push({ key: doc.id, ...doc.data() });
                });
                setList(items);
                setPage(page - 1)
            })
    };
    fetchPreviousData();
};

at the end create list view & two buttons like this

 {
    //list doc's here this will come inside return (place this code inside table)
    list.map((doc) => (
        <tr key={doc.key}>
            <td>{ doc.name }</td>
            <td>{ doc.age }</td>
            <td>{ doc.note }</td>
        </tr>
    ))
}


{
    //show previous button only when we have items
    //pass first item to showPrevious function 
    page === 1 ? '' : 
    <Button onClick={() => showPrevious({ item: list[0] }) }>Previous</Button>
}

{
    //show next button only when we have items
    //pass last item to showNext function 
    list.length < 5 ? '' :
    <Button onClick={() => showNext({ item: list[list.length - 1] })}>Next</Button>
}

That's it check my code comments where you can change as per your need. this is what happens when you paginate using Firebase FireStore. you can use create custom hook to reuse these component as per your need.

Hope this could help someone so i made a gist check it here

Solution 5:[5]

here AddTable and AddForm is adding table and add form to fill data in table...

  import React, { useEffect, useState } from "react";
  import Button from "react-bootstrap/Button";
  import Pagination from "react-bootstrap/Pagination";
  import AddTable from "../management/AddTable";
  import AddForm from "../management/AddSuperAdminForm";
  import {
    where,
    getDocs,
    collection,
    query,
    orderBy,
    startAfter,
    limit,
    endBefore,
    limitToLast,
  } from "firebase/firestore";
  import { db_firestore } from "../../../firebase.config";
  
  const SuperAdmin = () => {
    const [tableDataArray, setTableDataArray] = useState();
    const [show, setShow] = useState(false);
    const [editId, setEditId] = useState("");
    const [oldUid, setOldUid] = useState("");
    const [lastVisible, setLastVisible] = useState();
    const [prevVisible, setPrevVisible] = useState();
  
    const handleClose = () => {
      setShow(false);
      setEditId("");
    };
    const handleShow = () => {
      setShow(true);
      setEditId("");
    };
  
    let tempdata;
    let pageSize = 3;
    let q = query(
      collection(db_firestore, "users"),
      where("role", "==", "superadmin"),
      orderBy("timestamps", "desc"),
      limit(pageSize)
    );
    
    function nextPage(lastVisible) {
      q = query(
        collection(db_firestore, "users"),
        where("role", "==", "superadmin"),
        orderBy("timestamps", "desc"),
        startAfter(lastVisible),
        limit(pageSize)
      );
    }
  
    function prevPage(firstVisible) {
      q = query(
        collection(db_firestore, "users"),
        where("role", "==", "superadmin"),
        orderBy("timestamps", "desc"),
        endBefore(firstVisible),
        limitToLast(pageSize + 1)
      );
    }
  
    const newfun = async () => {
      const querySnapshot = await getDocs(q);
      tempdata = [];
    
      // Get the last visible document
      setLastVisible(querySnapshot.docs[querySnapshot.docs.length - 1]);
    
      // Get the prev visible document
      setPrevVisible(querySnapshot.docs[0]);
    
      querySnapshot.forEach((doc) => {
        const { name, email, uid } = doc.data();
      
        tempdata.push([name, email, uid, doc.id]);
      });
      console.log("SuperAdmin...");
      setTableDataArray(tempdata);
    };
  
    useEffect(() => {
      newfun();
      // setInterval(() => { // if you want to get new update after some secound
      // newfun();
      // }, 10000);
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);
  
    return (
      <div>
        <Button
          className="d-block mx-auto my-2"
          variant="primary"
          onClick={handleShow}
        >
          Add SuperAdmin
        </Button>
        {/*     -----> AddTable <------ 
        Index will generate Automatic In Table.
        Always keep action end of the table.
        */}
        {tableDataArray ? (
          <AddTable
            tableHeaders={["Name", "Email", "uid", "Action"]}
            tableData={tableDataArray}
            fetchNew={newfun}
            setEditId={setEditId}
            setShow={setShow}
            setOldUid={setOldUid}
          />
        ) : (
          ""
        )}
        <AddForm
          fetchNew={newfun}
          show={show}
          setShow={setShow}
          handleClose={handleClose}
          editId={editId}
          oldUid={oldUid}
        />
        <Pagination className="float-end">
          <Pagination.Item
            className="shadow-none"
            size="lg"
            onClick={() => {
              prevPage(prevVisible);
              newfun();
            }}
          >
            Previous
          </Pagination.Item>
          <Pagination.Item
            className="shadow-none"
            size="lg"
            onClick={() => {
              nextPage(lastVisible);
              newfun();
            }}
          >
            Next
          </Pagination.Item>
        </Pagination>
      </div>
    );
  };
  
  export default SuperAdmin;

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Joel Jerushan
Solution 3 Orkhan Jafarov
Solution 4 Joel Jerushan
Solution 5 Dhruv Sakariya