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.
To create the counter library, you would:
- Click File > New > Static Resource in the Developer Console.
- Implement the counter logic as follows:
- 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.
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.
Make sure you use the latest ltng:require syntax with $Resource as displayed in the code above. More information here.
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 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:
Creating Data Services
We could then create an AccountList component implemented as follows:
And the component controller would look like this:
Option 1 Summary
- Allows you to encapsulate logic you can share between components
- 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:
- 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:
- 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:
- 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:
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.
- Great for services that are backed by an Apex controller.
The source code for all the examples in this article are available in this GitHub repository.