Building Vue Applications with Nuxt.js Part 3: Firebase Pagination

SUBSCRIBE TO THE BLOG

This is the third part of a blog series that will introduce you to Nuxt.js, a higher-level framework that builds on top of Vue. In Part 1 of this tutorial, we created a base project that used Vuetify as a component library. In Part 2, we took a deeper dive by creating a short blog registration form and a Firebase database to handle our data.

In this entry, we will add pagination to our Firebase database. Given how data is structured within Firebase, pagination is not supported out of the box. So we’ll create a workaround for that, to retrieve only the data necessary in our table.

Paginating data on Firebase

When working with large data sets, we may need to use server-side pagination to keep our app running smoothly. Server-side pagination usually handles a page size, a page number, and an item count. On Firebase, however, pagination works a little differently.

Pagination on Firebase

Firebase pagination is geared towards infinite-scrolling-like scenarios, instead of traditional page number and page size pagination. This is great for mobile applications with infinite feeds; however we need some workarounds to get it working similarly to traditional pagination.

First, we need to add a way to retrieve data from the initial page, like this:


      sync loadGorillas({commit, state}, { sortBy = 'name', desc = false, page, itemsPerPage }){
        try {
            // Early exit if called with incomplete data
            if(!itemsPerPage){
                return
            }
            const sortDirection = desc ? 'desc' : 'asc';
            const dbRef = await this.$fire.firestore.collection('gorillas');
            // Bring an additional item to determine if there is a page next to the retrieved one.
            let query = dbRef.orderBy(sortBy, sortDirection).limit(itemsPerPage + 1);
...

Then we need to create a way to retrieve either the following page or the previous one:


     const nextPageQuery = (ref, last,  { sortDirection, sortBy, itemsPerPage }) => { 
    return ref.orderBy(sortBy, sortDirection).startAfter(last ? last[sortBy]: null).limit(itemsPerPage + 1);
}

const previousPageQuery = (ref, first, { sortDirection, sortBy, itemsPerPage }) => { 
    return ref.orderBy(sortBy, sortDirection).endBefore(first[sortBy]).limitToLast(itemsPerPage);
}

In Firebase, pagination is relative to a record value. This means we have to send a record to find the relative position from our query cursor.

To determine where the pagination is headed, we need to know where it previously was. For this, we create an attribute in our store to keep track of the page position called currentPage. 

And finally, we need to determine if there are any pages left, so we add another attribute to the store, hasNextPage, which allows us to track when to enable/disable pagination.

The final pagination code looks like this:


    async loadGorillas({commit, state}, { sortBy = 'name', desc = false, page, itemsPerPage }){
    try {
        // Early exit if called with incomplete data
        if(!itemsPerPage){
            return
        }

        // Determines the direction to sort the information in Firebase
        const sortDirection = desc ? 'desc' : 'asc';
        const dbRef = await this.$fire.firestore.collection('gorillas');
        
        // Bring an additional item to determine if there is a page next to the retrieved one.
        let query = dbRef.orderBy(sortBy, sortDirection).limit(itemsPerPage + 1);
        // Page has changed when the store's page doesn't match the page passed to the function
        const pageChange = state.currentPage !== page;
        // If the page passed to the function is lesser than the currentPage in the store, we want to retrieve the previous page
        const retrievePreviousPage = state.currentPage > page;

        // If we are moving backwards, we have a nextPage
        let hasNextPage = retrievePreviousPage;

        if(pageChange){
            const pagingFunction = retrievePreviousPage ? previousPageQuery: nextPageQuery ;
            // Items of the previous page are relative to the first element 
            // in our items array, items for the next page are relative to the last
            const referenceItem = retrievePreviousPage ? state.gorillas[0]: state.gorillas[state.gorillas.length - 1];
            query = pagingFunction(dbRef, referenceItem, {sortDirection, sortBy, itemsPerPage});
        }

        const dbSnapshot = await query.get();
        const gorillaList = dbSnapshot.docs.map(doc => ({... doc.data(), id: doc.id}));
        
        hasNextPage = hasNextPage || dbSnapshot.docs.length > itemsPerPage;
        if(hasNextPage && !retrievePreviousPage){
            // Remove the additional item
            gorillaList.pop();
        }
        
        commit("setHasNextPage", hasNextPage);
        commit("setCurrentPage", page);
        commit("setGorillas", gorillaList);
    } catch (e) {
        return Promise.reject(e)
    }
} 

Now we have everything we need to handle pagination. This method still has some quirks, such as when sorting by elements that have repeated data, as this can cause inconsistencies in the data retrieval.

Adapting the data table to server-side pagination

Now that we’ve prepared our data retrieval function for server-side pagination, we’ll modify the table to support server-side pagination.

First, we need to add two props to the data table component that will have a special workaround for Firebase pagination:


      <v-data-table
  ...
  :options.sync="paginationOptions" → new
  :server-items-length="serverSidePlaceholer" → new
  ...
  >

paginationOptions

This prop allows us to sync the internal state of the v-data-table pagination and use these props as input to our table. For these, we need to add the following to our Vue export:


      <script>
export default {
  data(){ 
      return { 
        // Initialization
        paginationOptions: {} 
  }
  watch: {
      // Syncing
      paginationOptions: {
        handler () {
          this.loadGorillas();
      },
      deep: true
      }
    },
  }
</script>

Now we can use the data table pagination props.

serverSidePlaceholder

Given that we can’t actually know how many items we have in our Firebase Firestore database, we need a way for the pagination to know when to enable the next button. For this we will use the store variable we previously created for hasNextPage:


     <script>
export default {
...
  computed(){ 
      return serverSidePlaceholer() {
        const { page, itemsPerPage } = this.paginationOptions;
        // Make the pagination think there are items in the next page, or the current length traversed
        return this.hasNextPage ? 
               (page * itemsPerPage) + 1 :
               ((page - 1) * itemsPerPage) + this.gorillas.length;
      }

  }
...
</script>

And with those workarounds, we can now load our paginated data as follows:


      async loadGorillas() {
        const { sortBy = [], sortDesc = [], page, itemsPerPage } = this.paginationOptions;
        await this.$store.dispatch('loadGorillas', { sortBy: sortBy[0], desc: sortDesc[0], page, itemsPerPage});
      }

Now we’ve implemented table server-side pagination on Vue with Firebase!

We have implemented firebase pagination on Vue

Restoring pagination to initial state

When modifying data using our server-side pagination implementation, we want to reset to the initial page to avoid data inconsistency issues:


         async resetPagination(){
        this.$store.commit("setCurrentPage", 1);
        this.paginationOptions.page = 1;
        await this.loadGorillas();
      }

Wrapping up our simple Nuxt.js: firebase pagination tutorial

We’re done! Remember that you can download and check the final repo here.

I hope that you enjoyed this Nuxt.js, Firebase, and Vuetify tutorial. Vuetify adds great usability to an already powerful framework, making it possible to create complex solutions with minimal effort. The customizability and wide array of components make it a perfect companion for projects of any size and difficulty.

New call-to-action

Angelo Ramirez Ortega

Angelo is a Full-stack developer living in Costa Rica. He is passionate about technology, travel, and everything sports-related.

Related Articles

Ready to be Unstoppable?

Partner with Gorilla Logic, and you can be.