Lightning Best Practice: Adding Pagination to Lists

Pagination

What happens to a Lightning Component that displays a list of data without pagination?

It is probably doomed to suffer from performance problems and who wants to build a component that is doomed? Certainly not you, right?

It’s possible that Salesforce might release a pagination component at some point in the future, but until they do, you will need to roll out your own. Fortunately, it is not too terribly complicated and in this post I will walk you through how to do it.

To begin, I must give credit where credit is due and acknowledge that the paginator component I am using in this post is almost identical to the one used in the Dreamhouse application (which if you have not checked out, you really need to do so).

The markup code for the Paginator component is as follows:

Paginator

And the Controller code looks like this:

({
	previousPage : function(component) {
        var pageChangeEvent = component.getEvent("pagePrevious");
        pageChangeEvent.fire();
	},
    
	nextPage : function(component) {
        var pageChangeEvent = component.getEvent("pageNext");
        pageChangeEvent.fire();
	}
})

Additionally, I use a component event called PageChange which looks like this:

PageChange
Ok, so I have a component that currently renders a list of race data. To make it work with the paginator component through, I will have to make a few changes to both the markup,  controller and helper, along with the Apex Controller it references.

The new version of the markup looks like this:

NewListRacesMarkup

The Modified controller (seen below) now includes two new actions named onPagePrevious and onPageNext and these are referenced in the Paginator component.

({
    doInit : function(component, event, helper) {
	helper.getRaces(component);
    },
    handleAddToRaces : function(component, event, helper) {
        helper.addToRaces(component, event);
    },
    onPagePrevious: function(component, event, helper) {
	var page = component.get("v.page") || 1;
        page = page - 1;
        helper.getRaces(component, page);
    },
    onPageNext: function(component, event, helper) {
	var page = component.get("v.page") || 1;
        page = page + 1;
        helper.getRaces(component, page);
    }
})

And the getRaces function in the Helper file has been modified to look like this:

// Added new parameter called page to pass in the page number
// If no page parameter is passed in, it will just default to 
// a value of 1 and this is the case on the initial call for 
// the doInit action
getRaces : function(component, page) {
    var action = component.get('c.getRacesDB');
    // Added the pageSize variable which is passed in as an attribute
    var pageSize = component.get("v.pageSize");
    // Added code to set the new parameters that are now passed on 
    // to the Apex Controller Code
    action.setParams({"pageSize": pageSize,
          	      "pageNumber": page || 1
    });
    action.setCallback(this, function(response) {
        var state = response.getState();
        if (component.isValid() && state === "SUCCESS") {
            // Instead of just returning all the data
            // as a list, I will get back a result
            // object which is defined in the Apex Controller
            var result = response.getReturnValue();
            component.set("v.races", result.races);
            // Added code to set the values for the page, 
            // total and pages attributes
            component.set("v.page", result.page);
            component.set("v.total", result.total);
            component.set("v.pages", Math.ceil(result.total/pageSize));
         } else {
            //Handle errors
            var errors = response.getError();
            if (errors) {
               if (errors[0] && errors[0].message) {
                   component.set("v.errorMsg", errors[0].message);
                   component.set("v.isError", true);
               }
            } else {
                component.set("v.errorMsg", "unknown error, response state: " + 
                             response.getState());
                component.set("v.isError", true);
            }
          }
        });
        $A.enqueueAction(action);

	},

The last thing to do is to modify the code in the Apex Server Controller, which will now look like this:

public with sharing class ListRacesController {

    @AuraEnabled
    // Changed the return value from List to PageResult
    // which is defined in the inner class below Also added two
    // new parameters for the pageSize and pageNumber
    public static PageResult getRacesDB(Decimal pageSize, Decimal pageNumber) {
    	// Added new variables
        Integer pSize = (Integer)pageSize;
        Integer offset = ((Integer)pageNumber - 1) * pSize;
        Integer totalRows = 0;
        
        // Instead of just returning a List of races from a single
        // query, we are now returning a PageResult
        PageResult res = new PageResult();
        res.pageSize = pSize;
        res.page = (Integer) pageNumber;
        
        // The first query is used to fetch the data that will
        // be displayed and it will be limited to return just 
        // the data for the particular page it needs to render
        res.races = [SELECT Id, Name, DateTime__c, Location__c,
		     Attended__c, Type__c, Results__c FROM Race__c
		     ORDER BY DateTime__c desc
		    LIMIT :pSize OFFSET :offset]; 
	// We have to do a separate aggregate query to get 
        // the total number of records since this will be
        // used to compute the offset
        res.total = [SELECT Count() FROM Race__c];
        
        return res;
       
    }
   
   // Added PageResult class which defines the 
   // results returned from the getRaces method
   public class PageResult {
        @AuraEnabled
        public Integer pageSize { get;set; }
        
        @AuraEnabled
        public Integer page { get;set; }
        
        @AuraEnabled
        public Integer total { get;set; }
        
        @AuraEnabled
        public List races { get;set; }
    }
}

And that’s it. I now have a component that will by default only display 5 races at a time and allow the user to move between the pages using the arrow buttons.

And now I can rest – assured that my component (which honestly could use some other improvements), will not perform miserably when the number of races eventually climbs to a very high number.

Pretty neat, right?

Want to learn about other improvements? Well, the next one is to add caching to this same component and believe it or not, I can do it with a single line of code. Check out this post for more info. And stay tuned because this blog will continue to feature lightning best practices such as these.

EDIT: Below is the requested markup and code for the inner RaceV2 component, which actually includes the individual race data.

First, the Markup:

RaceV2Markup.png

And now the Helper Resource:

({
	updateRace :function(component) {
        var race = component.get("v.race");
        console.log("Calling updateRace");
        var action = component.get("c.updateRaceDB");
        action.setParams({ "race" : race });
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (component.isValid() && state === "SUCCESS") {
                console.log("Race successfully updated");
            } else if (state === "ERROR") {
                var errors = response.getError();
                if (errors) {
                    if (errors[0] && errors[0].message) {
                        console.log("Error message: " + errors[0].message);
                    }
                } else {
                    console.log("Unknown error");
                }
            } else {
                console.log("Action State returned was: " + state);
            }

        });
        $A.enqueueAction(action);

        
        
    }
})

If you found this article useful, you might want to checkout my latest course on Pluralsight titled, Lightning Component Development Best Practices, where I talk about pagination and a lot more.

6 thoughts on “Lightning Best Practice: Adding Pagination to Lists

  1. I think usually most of the bottleneck is usually around rendering before then SOQL limits/speeds hit you. For many apps server side pagination is probably not really needed.

Leave a comment