Modularizing Code in Lightning Components

You can build simple Lightning Components that are entirely self-contained. However, if you build more complex applications, you’ll probably want to share code (and potentially client-side data) between components. In this article, we will explore strategies to share code between components, promote reuse, and avoid code duplication.

Before we start, here are a few examples of code you may want to share between components:

  • Code that provides access to a third party service such as Twitter, LinkedIn or Facebook (backed by a custom Apex controller that makes the callout to the service).
  • Code that provides access to IoT connected devices.
  • Collection of methods that provide general purpose coding utilities: formatting, conversion, etc (similar to moment.js for example).
  • Domain specific business logic (for example, financial formulas etc.).
  • Data Services that encapsulate data access logic in order to decouple the UI components from a specific data access strategy (data access abstraction layer).
  • Data Services that provide access to metadata (for example, picklist values).
  • Transient services that provide access to a shared/non-persistent data model and some related methods. For example, a MortgageCalculator service that exposes a transient data model (principal, rate, term) and related methods.

With these examples in mind, let’s take a close look at two code sharing strategies.

Option 1: Sharing Code Using a JavaScript Library

You have probably already used ltng:require to leverage third party libraries (like leaflet and others) inside your components. You can also use ltng:require to import your own JavaScript libraries into your components. As an example let’s create a very simple counter library that provides a getValue() method that returns the current value of the counter, and an increment() method that increments the value of that counter.

Creating the JavaScript Library

To create the counter library, you would:

  1. Click File > New > Static Resource in the Developer Console.
  2. Specify counter, as the name, and text/javascript as the MIME Type, and click Submit.
  3. Implement the counter logic as follows:
    window.counter = (function(){
    
        var value = 0; // private
    
        return { //public API
            
            increment: function() {
                value = value + 1;
                return value;
            },
    
            getValue: function() {
                return value;
            }
            
        };
    
    }());
    

Code highlights:

  • There are of course different ways to structure JavaScript libraries. The code above uses the JavaScript module pattern. Using this closure-based pattern, the value variable remains private to your library: it can’t be accessed directly by components using the library.
  • You could also use libraries you package with build tools like Webpack, Browserify, and Rollup using the Universal Module Definition (UMD) format.
  • Even though window.counter looks like a global declaration, counter is attached to the LockerService secure window object and therefore is a namespace variable, not a global variable.

Using the JavaScript Library

Now that we created the counter library, we can use ltng:require to import it into Lightning Components. As an example, let’s create a MyCounter component that provides a simple UI that exercises the counter methods.

Component:

<aura:component implements="flexipage:availableForAllPageTypes"
                access="global">
    
    <ltng:require scripts="{!$Resource.counter}"
                  afterScriptsLoaded="{!c.getValue}"/>

    <aura:attribute name="value" type="Integer"/>

    <lightning:card title="My Counter">
        <aura:set attribute="actions">
            <lightning:button label="Get Value" onclick="{!c.getValue}"/>
            <lightning:button label="Increment" onclick="{!c.increment}"/>
        </aura:set>
        <h1 class="slds-align--absolute-center">{!v.value}</h1>
    </lightning:card>
    
</aura:component>

Make sure you use the latest ltng:require syntax with $Resource as displayed in the code above. More information here.

Controller:

({
    getValue : function(component, event, helper) {
        component.set("v.value", counter.getValue());
    },
    
    increment : function(component, event, helper) {
        component.set("v.value", counter.increment());
    }
})

Code Highlight: counter.getValue() and counter.increment() are calls to your counter library.

Singleton vs Per-Component Instance

In the specific implementation above, all the components use a single instance of the counter service. That means that the counter value is shared between all the components using that library. Depending of what you are building, that may or may not be what you want. If you needed each component using the library to have its own counter value, you could easily modify the implementation of the counter library as follows:

window.Counter = function() {

    var value = 0;
    
    var increment = function() {
        value = value + 1;
        return value;
    };
    
    var getValue = function() {
        return value;
    };
    
    return { // public API
        increment: increment,
        getValue: getValue
    };

};

Code Highlights:

  • window.Counter is now a constructor function that components can use to create an instance of the counter service using the new operator (see below).
  • You could also architect your service to support both the singleton and the per-instance scope.

We can now modify the MyCounter component to create a new instance of the counter service in the afterScriptsLoaded event and store it in a new attribute called counter. The component would now look like this:

Component:

<aura:component implements="flexipage:availableForAllPageTypes"
                access="global">
    
    <ltng:require scripts="{!$Resource.counterfactory}"
                  afterScriptsLoaded="{!c.afterScriptsLoaded}"/>

    <aura:attribute name="value" type="Integer" default="0"/>
    <aura:attribute name="counter" type="Object" access="private"/>

    <lightning:card title="Counter Factory">
        <aura:set attribute="actions">
            <lightning:button label="Get Value" onclick="{!c.getValue}"/>
            <lightning:button label="Increment" onclick="{!c.increment}"/>
        </aura:set>
	<h1 class="slds-align--absolute-center">{!v.value}</h1>
    </lightning:card>
    
</aura:component>

Controller:

({
    afterScriptsLoaded : function(component, event, helper) {
        component.set("v.counter", new Counter());
    },

    getValue : function(component, event, helper) {
        var counter = component.get("v.counter");
        component.set("v.value", counter.getValue());
    },
    
    increment : function(component, event, helper) {
        var counter = component.get("v.counter");
        component.set("v.value", counter.increment());
    }
})

Creating Data Services

You could also use the JavaScript library approach to create data services that encapsulate data access logic in order to decouple the component from a specific data access mechanism, or simply to avoid duplicating data access logic across components. As an example, let’s create an AccountService library that encapsulates the data access logic to retrieve a list of accounts.

First, let’s create a JavaScript library named AccountService (File > New > Static Resource) implemented as follows:

window.AccountService = function(component) {

    return {
        
        findAll: function(callback) {
            var action = component.get("c.getAccounts");
            action.setCallback(this, function(response) {
                if (response.getState() === "SUCCESS") {
                    callback(null, response.getReturnValue());
                } else {
                    callback(response.getError());
                }
            });
            $A.enqueueAction(action);
        },

        findByName: function(name, callback) {
            // implementation here
        },
        
        findById: function(id, callback) {
            // implementation here
        },
        
    };

}

We could then create an AccountList component implemented as follows:

<aura:component controller="AccountController">

    <ltng:require scripts="{!$Resource.AccountService}"
                  afterScriptsLoaded="{!c.afterScriptsLoaded}"/>

    <aura:attribute name="accounts" type="Account[]"/>

    <ul>
    <aura:iteration items="{!v.accounts}" var="account">
    	<li>{!account.Name}</li>
    </aura:iteration>
    </ul>        

</aura:component>

And the component controller would look like this:

({
    afterScriptsLoaded: function(component, event, helper) {
		
        var service = new AccountService(component);    
        service.findAll($A.getCallback(function(error, data) {
            // TODO: Implement error handling
            component.set("v.accounts", data);                           
        }));
        
    }
})

Option 1 Summary

  • Allows you to encapsulate logic you can share between components
  • Lightweight
  • Allows for singleton instance or per-component instances
  • Easy to share data between components at the client-side (using a singleton instance)
  • Less straightforward access to Lightning Components infrastructure (for example, Lightning Data Service)
  • If the library is backed by an Apex controller, that controller must be declared on the components that use the library (<aura:controller controller=”MyController”>). That requirement makes it harder for a component to work with multiple libraries that are backed by different Apex controllers. The solution, in that case, is to “front-end” the different Apex controllers with a single Apex controller (assigned to the Lightning Component) that then delegates the different methods to the appropriate controllers. Another solution to this specific situation is to use option 2 described below.

Option 2: Sharing Code Using a Service Component

The first Lightning Components you built were probably visual components: they exposed a User Interface that you built using UI controls like <lightning:input>, <lightning:button>, etc. But you can also build Lightning Components that don’t have a UI, and that are used to encapsulate logic that you want to reuse in other components. These non-visual components are often called “service components.” You build a service component the same way you build a UI component. The only difference is that it doesn’t have any UI markup. You can then drop your service component in other Lightning Components that need the service it provides. To facilitate communication, you can use <aura:method> to provide your service component with a public API that the parent component can call directly.

As an example, let’s create a service component implementation for the AccountService we created in option 1.

The AccountService component markup could look like this:

<aura:component controller="AccountController">

    <aura:method name="findAll" action="{!c.findAll}">
        <aura:attribute name="callback" type="function"/>
    </aura:method>

</aura:component>

Code highlights:

  • No UI markup
  • The service component has a direct reference to the backing Apex controller (<aura:component controller=”AccountController”>)

And the component’s controller could look like this:

({
    findAll : function(component, event, helper) {
        var params = event.getParam("arguments");
        var action = component.get("c.getAccounts");
        action.setCallback(this, function(response) {
            if (response.getState() === "SUCCESS") {
                params.callback(null, response.getReturnValue());
            } else {
                params.callback(response.getError());
            }
        });
    	$A.enqueueAction(action);
    }
})

Code highlights:

  • For brevity in this example, AccountService exposes a single method (findAll). In a real life application, you’d probably expose additional methods (findByName, findById, etc).
  • When building this type of service, consider using storable actions to enable client-side caching. Note that once you enable caching, you must also have a cache invalidation strategy.

Now that we defined the AccountService component, we can drop it in other components that need a list of accounts. For example, you could create an AccountList component implemented as follows:

<aura:component>

    <aura:attribute name="accounts" type="Account[]"/>

    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    
    <c:AccountService aura:id="service"/>

    <ul>
    <aura:iteration items="{!v.accounts}" var="account">
    	<li>{!account.Name}</li>
    </aura:iteration>
    </ul>        
    
</aura:component>

Code Highlights:

  • Note that you no longer have to declare the Apex controller on <aura:component>. The data access logic is fully encapsulated in AccountService.

And the AccountList controller could be implemented like this:

({
    doInit : function(component, event, helper) {
        var service = component.find("service");
        service.findAll(function(error, data) {
            // TODO: Add error handling
            component.set("v.accounts", data);
        });
    }
})

Option 2 Summary

  • Allows you to encapsulate logic you can share between components
  • You can use multiple service components inside a Lightning Component, allowing you to essentially work with multiple Apex controllers from within the same component.
  • Allows you to use other declarative constructs of the framework such as event registration and handlers, data service (<force:recordPreview>), etc.
  • Each parent component has its own instance of the service component. Sharing client-side data is therefore not always achievable with this approach. If that is a requirement, you could use option 1 (singleton version), or combine both approaches: use component services that load (ltng:require) a JavaScript library that provides a singleton instance of a service.
  • Great for services that are backed by an Apex controller.

Source Code

The source code for all the examples in this article are available in this GitHub repository.

Summary

Service Components and JavaScript libraries allow you to share code between components, promote reuse, and avoid code duplication. You can change the internal implementation of shared services without any impact on components that use them as long as the public API doesn’t change. The Service Component approach is particularly well suited for services that are backed by an Apex controller. You can use both approaches in the same application, or even combine them (Service Component loading a JavaScript library) to leverage the right approach for the right task.

tagged , , Bookmark the permalink. Trackbacks are closed, but you can post a comment.
  • Very well written and enlightening post for using modular code in JS. Much needed post. Thanks Christophe.

  • Thanks Christoph – this came right in time for me. I was struggling to get a hang on managing a lot of components and my issues have suddenly disappeared. My Christmas wish: more advanced input like your article or John Belo’s recent webinar on Advanced Lightning Apps (https://developer.salesforce.com/events/webinars/AdvLightning)

  • Hemavantha Rajesh Varma Mudunu

    Great post. very efficient way of reusing your javascript across components

  • This is great blog post .Well articulated and great examples .Thank you for this helpful content

  • Ankush Somani

    An indeed post for lightning Devloper. A great escape from unwanted code

  • davcondev

    Option 3 inheritance?