Lightning Best Practice: Storable Actions

File_Server_Cache_Blue

Storable Actions are a GREAT way to enhance the performance of your Lightning components and implementing them is incredibly easy, but there is one big gotcha to seeing them work with a stand-alone Lightning Application.

Even though storable actions are automatically configured in Lightning Experience and Salesforce1, any Stand-alone Lightning apps that are used to host your components will NOT use caching by default.

For these apps to use storable actions, you must do these two things:

1.) Add a new component which you can name whatever you want, but I named mine AppTemplateComponent and have it use the following code:

AppTemplateMarkup

2.) You then need to add the following attribute to the aura:application tag of your Lightning Standalone app:

template="c:AppTemplateComponent"

Once you do this, you should be able to see the caching working for any component used in that app that calls the following line of code:

action.setStorable();

Isn’t that amazing?

Just one line of code is all you need to add to start taking advantage of caching in your server-side actions. The impact on performance can be amazing and perhaps the best place to see is when it comes to rendering a list of data (especially if that data is non-mutable and therefore will not be updated).

This best practice really is a no brainer that everyone building Lightning Components should consider using.

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 storable actions and a lot more.

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.

Do this Salesforce Security Scan NOW!!!

Screen Shot 2016-08-20 at 11.39.10 AMPrior to about a week ago, if you went to this page and requested a Force.com Security Source Code Scan, you would have most likely gotten an error telling you that it could not be done and that you would need to submit a case.

Luckily, Salesforce has resolved this issue and you can now simply go to the link above, enter your credentials and you will be able to scan your org for security and quality rules. The scan will take a while to run (as in several hours or even days perhaps), but I promise it is worth the wait.

What you will get back is a very thorough report that scans your entire codebase for not only security issues (of which I am sure you will be amazed by how many critical violations it will find) but it will also evaluate whether you are utilizing best practices in your code to ensure good quality.

For example, it will scan your code to tell you where you have code that does the following:

  • Queries With No Where Or Limit Clause
  • Multiple Trigger On same sObject
  • Hardcoded Ids
  • DML Statements Inside Loops
  • SOSL SOQL Statements Inside Loops
  • Async Future Method Inside Loops
  • Test Methods With No Assert
  • Need to Bulkify Apex Methods Using Collections In Methods

I think everyone should take the time to have their orgs scanned and review the results. I suspect that even the most diligent of development shops will find some issue that needs to be addressed.

Happy Scanning.