10 Tips for Implementing an Efficient Lightning Lookup Component

A Lightning lookup component is a text field with autocompletion and a dropdown. There are several different ways to create these for Lightning Experience, but a few best practices emerge.

In this blog post, I will present ten technical decisions and best practices that drove the implementation of a custom Lightning Lookup component that I contributed. Even if you do not plan to build your own lookup component, you may learn valuable information that you can apply to other projects.

Here is an overview of the themes that we will cover:

  • Third-party libraries
  • Salesforce Lightning Design System
  • Conditional rendering methods
  • Performance and scalability
  • Search optimization
  • Flexible and reusable architecture

Avoid using third-party libraries for basic UI needs

When building a custom lookup component, it can be quite tempting to go for simplicity and grab a component from a third-party library. For example, there are quite a few examples of components using jQuery or AngularJS.

Although this is convenient because it reduces the development effort, keep in mind that loading such libraries adds an overhead that affects performance on the client side. This extra cost is not justified when displaying a simple lookup component because you can easily achieve this with basic HTML and JS. As a side note, using a third-party library may also prove problematic when styling your component.

I recommend that you only use third-party libraries when the requirements are complex enough to justify it (e.g. displaying maps or charts).

Use the Salesforce Lightning Design System

If you create a custom component you should use the Salesforce Lightning Design System (SLDS). It guarantees that your component will have the proper style, be responsive and be accessible.

SLDS provides component blueprints that allow you to start from a static HTML template that has the proper CSS classes applied to it. Creating a component is a matter of copying the blueprint and turning into something dynamic with some JS.

One of the important bits to remember when copying blueprints is to maintain the accessibility of your component by generating dynamic unique IDs. This allows assistive technology such as screen readers to link labels with their corresponding fields. It helps disabled users to access and use your component.

You can easily achieve this by replacing the provided hard-coded IDs with expressions like this:

<label for="{# globalId + '_input' }" ...>...</label>
...
<input type="text" id="{# globalId + '_input' }" .../>
...

In this sample code, we use an unbound expression combined with the globalId keyword to generate unique IDs even if there are multiple instances of our component on the same page.

The value of an unbound expression is never re-evaluated.

The unbound expression denoted by a # symbol is used to generate a constant value. Unlike bound expressions denoted by the ! symbol, the value of an unbound expression is never re-evaluated. That holds true even when the value of attributes that compose the expression changes. This static behavior brings a performance gain.

We also use the globalId keyword in our expression to retrieve a unique identifier for our component instance. We concatenate it with a suffix to identify our input.

Pick the optimal conditional rendering method

There are two main methods for conditional rendering and each has its advantages and drawbacks so you want to use them in the right context.

Aura:if tag

You may dynamically render a section of a page or a component by enclosing it in an aura:if tag. Although this syntax is easy to read, it has some performance implications. When hiding or displaying content with an aura:if tag, the Lightning component framework destroys and rebuilds the underlying DOM structure and these operations imply a UI rendering cost. This is probably an overkill if you are frequently hiding and displaying a portion of your UI.

For example, the use of aura:if is perfectly adapted for toggling between a single or multiple entry lookup layout because this configuration is unlikely to change:

<aura:if isTrue="{!v.isMultiEntry}">
    <!-- Multi entry lookup code goes here -->
    
    <aura:set attribute="else">
        <!-- Single entry lookup code goes here -->
    </aura:set>
</aura:if>

Dynamic CSS classes

The second option for conditional rendering is to use dynamic CSS classes. This option is preferred when we often toggle the visibility of a section because it does not destroy or rebuild the DOM.

For example, we should avoid using aura:if to conditionally display the lookup dropdown because we are likely going to toggle it often. Instead, we can achieve the same visual result in a cheaper way by toggling the dropdown visibility with CSS.

This can be achieved in two different ways: via an expression and a ternary operators or via JS.

The use of an expression can be convenient because it is a compact syntax. However, the ternary operators is generally harder to read:

<div class="{! 'slds-combobox '+ (and(v.hasFocus, !empty(v.searchResults)) ? 'slds-is-open' : 'slds-combobox-lookup') }">

The alternative to expressions is to use JS to toggle a class with the $A.util.toggleClass(component, 'class') function. This is powerful but the downside of this approach is that you will have to duplicate this statement across your controller or helper code if there are multiple conditions that can toggle the class.

Build something that scales

A poorly coded lookup component can have a severe performance impact on the server because of the number of search requests generated by the autocompletion. You may not notice it while you are still developing and testing your component in an isolated environment but the problem will arise when hundreds of users will generate thousands of search queries concurrently.

It is critical to design such a component with scalability in mind from the start. There are two things that you need do to ensure that your component scales.

Use storable actions

The first thing that needs to be done is to leverage the client-side cache so that you do not make unneeded server requests when searching for the same terms. Fortunately for us, this can easily be achieved by using storable actions. All you need to do to benefit from this cache is to call action.setStorable() in your JS before enqueuing your server-side action.

// Get action instance
// ('search' is an @AuraEnabled method on the Apex controller)
const action = component.get("c.search");

// Set action parameters
action.setParams({ searchTerm : searchTerm });

// Create a callback to handle action response
action.setCallback(this, function(response) {
    // Process response here
});

// Set action as storable to enable client-side cache
action.setStorable();

// Enqueue action for execution
$A.enqueueAction(action);

In order to get the most out of the client-side cache you should also clean your search term before issuing a search request.
To do so, you can apply the following code to

  • get rid of whitespaces and wildcard characters
  • force the case of the keywords
searchTerm.trim().replace(/*/g).toLowerCase();

This will ensure that search terms like “Astro,” “astro*” or “Astro ” (with a trailing space) all hit the same cached response.

Throttle search requests

Using the client-side cache is the bare minimum but you should go further by implementing a mechanism that throttles search requests. If you do not do so, every letter typed by the user will generate its own search query and will bombard the server with unneeded requests.

The average person types 200 characters per minute — that translates to 300 milliseconds per character. If you do not throttle search requests and you type a search term like “Codey,” you will trigger five — and mostly useless — search queries in less than 1.5 seconds. Imagine the negative performance impact of this behavior when you multiply it by the number of concurrent user searches — it clearly does not scale.

An optimal throttling mechanism should prevent the search from occurring while the user is still typing. You want to force a minimum delay between the typing of a letter and the resulting search request. The right way to achieve this is to use a timeout function in your JS and to run the search in an asynchronous manner as illustrated in this sample code:

onSearchTermChange : function(component, searchTerm) {
    // Cleanup search term and check if we need to
    // proceed with search (has the search term changed?)
    
    // Cancel previous timeout if any
    let searchTimeout = component.get('v.searchTimeout');
    if (searchTimeout) {
        clearTimeout(searchTimeout);
    }
    // Set new timeout
    searchTimeout = window.setTimeout(
        $A.getCallback(() => {
            // Send search request
            
            // Clear timeout
            component.set('v.searchTimeout', null);
        }),
        300 // Wait for 300 ms before sending search request
    );
    component.set('v.searchTimeout', searchTimeout);
}

Optimize search on the server side

The core functionality of a lookup component relies on the search that occurs on the server side. There are number of things that need to be done to optimize this search.

Use SOSL not SOQL

The first and most important thing that you need to do when implementing your search in Apex is to use the right querying language for the task that is SOSL (Salesforce Object Search Language) in this case and not SOQL (Salesforce Object Query Language). That one letter difference means a lot because these languages run on different technical stacks.

SOQL is great for searching on structured data like numbers, dates, checkboxes or picklist values. It can also search on text but its performance does not match the one of SOSL for that use case case.

SOSL is specifically designed to search on unstructured textual content because of the way its underlying technology indexes data. It can also return results from multiple and unrelated object types. SOSL also has the huge advantage of allowing you to search on all of text fields without enumerating them in your search query. This is especially valuable when searching on multiple object types:

FIND :searchTerm IN ALL FIELDS RETURNING
  Account (Id, Name, BillingCity),
  Opportunity (Id, Name, StageName)

Limit the number of results

Regardless of the querying language you use, you should always include a LIMIT statement in your query. This caps the maximum number of results that can be returned by the server and boost the query’s performance.

Another reason why you should include LIMIT in your query is that you will also have a constraint on the maximum number of lines that are displayed in the lookup dropdown. There is no need to fetch more data than what you can display.

FIND :searchTerm IN ALL FIELDS RETURNING
  Account (Id, Name, BillingCity),
  Opportunity (Id, Name, StageName)
  LIMIT :MAX_RESULTS;

Exclude items that are already selected

If working with a multiple entry lookup component, you need to exclude the items that you have already selected from the search results. If you do not do that, you may be stuck when trying to select new items if you reach the limit imposed by the query.

Excluding the selected items involves sending back a list of item IDs along with your search requests and filtering your queries to exclude those IDs. You can achieve that filtering by adding NOT IN :selectedIds to your SOSL query where selectedIds is the list of selected item IDs.

FIND :searchTerm IN ALL FIELDS RETURNING
  Account (Id, Name, BillingCity WHERE id NOT IN :selectedIds),
  Opportunity (Id, Name, StageName WHERE id NOT IN :selectedIds)
  LIMIT :MAX_RESULTS

Deliver a flexible and reusable component

Don’t assume that people will always search on all standard objects like Accounts or Opportunities. There is a good chance that they need to search on custom objects and that they also need to filter the list of selectable records based on certain conditions.

To ensure that, you need to preserve the decoupling between the UI and the Apex search code. In other words you need to make sure that your lookup component is not tied to a given Apex controller.

The way to achieve that is to call the search server action from a component that is a parent of the lookup:

  1. The lookup component notifies its parent when a new search request should be sent via a component event.
  2. The parent component calls a search method in its Apex controller and fetches the results.
  3. The parent controller injects the search results in the lookup component via an Aura method.

With this strategy you can easily have dedicated search methods for each of your lookup instances while keeping the lookup component totally generic and reusable. We will share more on this type of architecture in an upcoming post on Lightning Service components.

For example, let’s assume that you are building a form that includes two lookups that allow to search on:

  • closed accounts
  • a custom Machine__c object with an ‘under maintenance’ status

You can use two instances of the generic lookup in the parent form component. Each lookup can then be mapped to a specific Apex method like searchClosedAccounts and searchMachinesUnderMaintenance.

Closing words

A lookup component may seem trivial at first glance but there a number of pitfalls that need to be avoided when building one. Failing to properly design a component that scales will have a negative impact on performance (this is even more critical for a lookup component). Follow the best practices from this article and put your knowledge at work when creating custom Lightning components:

  1. Avoid using third-party libraries for basic UI needs.
  2. Use the Salesforce Lightning Design System.
  3. Use aura:if tag when displaying a section that is not likely to be toggled often.
  4. Use dynamic CSS classes when frequently toggling the visibility of a section.
  5. Use Aura storable actions to leverage client-side cache.
  6. Throttle search requests on the client-side.
  7. Use SOSL — not SOQL — to perform search queries.
  8. Limit the number of results returned by your search query.
  9. Exclude items that are already selected from search results.
  10. Deliver a flexible and reusable component.

If you wish to learn more about these tips and tricks by looking at some source code or if you just want to use a lookup component, feel free to check out my Open Source contribution.

For more learning on Trailhead, check out the Lightning Component Tips and Gotchas module.

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.