As the Lightning ecosystem evolves, I have noticed and adopted a valuable architecture pattern: Lightning service components. In this post, I will present this concept, illustrate how it is increasingly used in base components and provide tips and best practices on how developers can create their own service components.
Understanding Lightning service components
Before diving into the specifics of Lightning service components, let’s define what a service component is: A service component is a component that provides an API for a set of functionalities. Ideally, the service should be specialized, generic and reusable.
Another important thing that differentiates a service component from other components is the fact that it does not have a graphical representation. Unlike other components, it is not visible by default. However, it can display some graphics (like a modal dialog or a toast notification) upon request if it’s a UI service.
We will use the term “caller” throughout this post to designate a component calling a service.
An overview of base service components
Since the introduction of the Lightning Data Service (the first base service component) in Winter ’17, the number of base service components has steadily increased over releases. As of Summer ’18 there are 13 base service components and this number is likely to grow in the upcoming releases.
Summer ’18 base service components
Here is an overview of the Summer ’18 base service components. Notice that the terms “library,” “API” and “service” are used interchangeably but these are all service components.
|Lightning Data Service
|Provides the ability to create, read, update, and delete Salesforce records in Lightning without the use of an Apex controller.
|Displays messages via notices and toasts.
|Displays messages via modals and popovers.
|API for accessing/manipulating workspaces (Tabs and Subtabs).
|Utility Bar API
|API for the Utility Bar.
|Allows to navigate to a given page or to generate a page URL.
|Navigation Item API
|Allows to control navigation items in Lightning console apps, where navigation items display in an item menu.
|Quick Action API
|Allows to control actions in Lightning Experience on record pages.
|Console integration API for Live Agent.
|Omni-Channel Toolkit API
|Provides access to the API for the Omni-channel toolkit.
|Enables customization of the user interface for the minimized snap-in in Snap-ins for web.
|Enables customization of the user interface for the pre-chat page in Snap-ins Chat.
|Enables to fetch certain settings from within custom components for Snap-ins for web.
Using a base service component
All of the base components are documented with examples. For the sake of brevity we won’t examine all of them in detail in this post but let’s have a look at an example: displaying a notification with the notifications library.
The first step in using the notifications library or any other service is to add the service component to a caller component’s markup:
Notice that we included an aura:id attribute in our
lightning:notificationsLibrary tag. This attribute allows us to declare a local ID that identifies the component as
notifLib. This helps us to get programmatic access to the service component later.
The second step is to call our service with some JS code in our caller component’s controller. In our example we have a button that triggers a
handleShowNotice controller function. This function displays the notification via the service.
The first thing that our function does is to retrieve a reference to our service component by using the find method with our local id (
notifLib). Once we have the reference, we can then call an Aura method that provides the service. In our case, we call the
showNotice method with some parameters (an object with properties that describe the notification). This displays a notification.
This pattern of declaring a service component in markup and executing its Aura methods is the key to using service components.
Implementing and using a custom service component
In addition to the growing list of base service components, you may want to develop custom services. Here are a few simple steps on how to proceed:
- Identify a custom service candidate
- Implement the service
- Use the service
Identify a custom service candidate
When looking for a custom service candidate, you want to ask yourself a couple of questions such as:
- Is the JS code of your existing component’s “polluted” by some non-core functions?
- Do you find yourself writing the same bit of code over and over again?
- Can some of your code be reused?
Creating a custom service will simplify your component’s code and help you reduce code duplication. This in turns makes your code more robust and facilitates maintenance.
Let’s take a concrete example: calling a server-side action. This is a great match for a service as we all have copied the documentation’s JS template over and over again. However, the fact is that out of the hundred of lines of code that this represents (if you handle errors thoroughly), there is only one block of instructions that really varies: processing the server response.
This is why I contributed a Server Action Service, an ideal candidate for a service component. This service reduces the hundreds of lines of code required to call a server action and handle errors to about ten lines for complex cases (we will provide this code later in this article):
For simple use cases, server calls can even be inlined:
Using this service lets you focus on what matters the most and also takes care of handling errors in a unified way (displaying an error toast notifications and detailed error logs in the developer console).
Implement the custom service
When implementing a custom service, there are a couple of architectural approaches that you can explore. Here are some tips on how to adopt the best implementation strategies.
Injecting your service
A simple approach based on object-oriented programming is to use inheritance to inject your service. In this approach, the caller component inherits from the service:
However, inheritance is not the best service injection strategy for a couple of reasons:
- A component can only inherit from a single parent so this hampers your ability to use multiple services in the same component.
- Lightning component inheritance has some limitations.
The best alternative to inheritance is to use composition. That is, declaring an instance of a service in the component that will use it. This lets you declare and use several services in the same caller component:
Exposing your service
Once you have settled on how to inject the service, you need to decide how to expose its functionalities. There are two options: using events or a combination of Aura methods and callback functions.
Using events may seem the easiest way to call a service because of the low coupling that it implies:
- if using an application event, a service instance can be called from anywhere in the Lightning app.
- if using a component event, a service instance can be called by any component in the ancestry branch (parent or child) of its container.
However, there are some important downsides to the use of events:
- Your service has to be a singleton (there can be only one instance of it) within the “reach” of the event. If not, the event will be handled by multiple service instances and every service call will be duplicated.
- Events do not offer an easy way to provide callbacks (it lets you trigger the service but you cannot get a response from it).
A more flexible approach is to use the combination of an Aura method with one or several callback functions. Interestingly, Aura methods are the only place where you can define an Aura attribute of type
Function. This is extremely convenient as it allows to implement methods that return results asynchronously.
For example, in the server action service, we have a
callServer Aura method which specifies two callback functions
The component that calls the service executes the
callServer method by specifying a success and an error callback function (the code of these functions lives in the calling component). Then the service executes itself and calls the appropriate callback function with some results or errors.
Making your service generic by decoupling it from a specific Apex controller
Some services like the server action service require access to an Apex controller. This implies a certain level of coupling between the service component and it’s server controller. Unfortunately, such coupling goes against the principle of having generic services so we must break it. There are a two of ways to achieve this: moving the server controller out of the service component or implementing a generic Apex controller.
In the context of the server action service, there is no way to implement a truly generic Apex controller so we have to avoid declaring the controller in the service. In order to do that, we let the calling component specify an Apex controller. We then obtain a reference to a server action and pass it to the server action service.
Here are the four steps that illustrate how the server action service works (see diagram above):
- Calling component controller gets a reference to the server-side action (the Apex
- Calling component controller executes the service’s
callServerAura method with:
- the server-side action reference
- a reference to a success callback function
- optionally a reference to an error callback function
- Service executes the server-side action and gets the server response
- Service either
- calls the success callback function with the server response
- calls the error callback function with the server error and handles error notification
An alternative to this approach is to implement a generic Apex controller. For example, I have contributed a picklist service that retrieves the entries of any standard or custom picklist field. In such use cases, you do not want to create as many server actions as there are picklist types because this is a repetitive manual process. Plus, it is unmaintainable and does not scale well.
The solution to that problem is to implement a single server-side action but to make it fully generic by leveraging dynamic Apex and dynamic class instantiation. The complexity of the Apex code is beyond the scope of this post but here is the signature of the generic server-side action that returns picklist entries:
This allows the caller component to call the service with any picklist field:
While this dynamic Apex approach is extremely useful, bear in mind that it has the drawback of being slightly unsafe as it defeats compiler checks. You could be calling a service on a class or object that does not exist but the compiler will not throw an error when you save your code. You will only get an error at run time so this is harder to test and debug.
Use the custom service
Using a custom service is similar to using a base service component. To recap, these are the three steps:
- Add the service component with an
aura:idin a caller component’s markup.
- Get a reference to the service by calling
component.find()with the previously defined
aura:idin the caller’s JS controller or helper.
- Execute one of the service’s Aura methods.
As you have seen by now, Lightning service components are becoming more prevalent in the Salesforce Platform. This architecture pattern is an efficient and safe way to reuse code across different projects. Give it a try by using one of the base service components and move to the next step by creating your own!
Over the course of my different projects I have assembled a list of services that I often reuse. Feel free to check out my contributions:
- Server Action Service – A service that calls server-side actions.
- Picklist Service – A service that retrieves any picklist entries without the need to declare a custom Apex controller.
Learn more about how to write object-agnostic Apex for your Lightning components with the Build Reusable Lightning Components Trailhead project.
About the author
Philippe Ozil is a Senior Developer Evangelist at Salesforce where he focuses on the Salesforce Platform. He writes technical content and speaks frequently at conferences. He is a full stack developer and a VR expert. Prior to joining Salesforce, he worked as a developer and consultant in the Business Process Management (BPM) space. Follow him on Twitter @PhilippeOzil or check his GitHub projects @pozil.