Infinite/Lazy loading in lightning-datatable in LWC

Use Case:

We have been using lightning-datatable to a great extent to show related records or similar functionality. But ever wondered, what if the records shown using lightning-datatable are comparatively too large. 
Let's say, we are architecting a requirement which says to show all the Accounts in the system with some basic related information. What will be your first approach to meet this requirement? Well, there could be many solutions but let's focus two prominent approaches:
  1. Pagination
  2. Infinite Loading
Though both have their own advantages, infinite loading is more promising if we have to show the records and as the very idea of this blog is to know about infinite loading, let's emphasize on infinite loading approach. But how to achieve the infinite loading in lightning-datatable in LWC?

Implementation:

Infinite Loading Demystified: The very idea behind the infinite loading in lightning-datatable comes from the fact that when a user scrolls down to the end of the results, it fetches more records to append to existing set of displayed records. Assuming our use case of showing all Accounts in the system, we are showing 20 account records at a time to a user and when user has scrolled down all the way to the end of the records, next lot of 20 fresh records are fetched, appended to existing records and displayed on UI and hence making the total displayed records count to 40 and so on. 
More in a nutshell, infinite scrolling enables you to load a subset of data and then display more when users scroll to the end of the table. To enable infinite scrolling, specify enable-infinite-loading and provide an event handler using onloadmore on lightning-datatable. By default, data loading is triggered when you scroll down to 20px from the bottom of the table, but the offset can be changed using the load-more-offset attribute. Please go through the lightning-datatable documentation to know more.

Infinite Loading Approaches: There are couple of ways to implement infinite loading. Below are the widely used approaches.
  • Using OFFSET:  Typically this is the best approach for infinite loading if we know well in advance that the records being rendered by lightning-datatable are well under 2000 since we have certain limitations in using OFFSET in SOQL. To know more on how do we use OFFSET in SOQL and limitations, please go through the official documentation.
  • Using Wrapper Class: If we are to implement the infinite loading for more than 2000 records, OFFSET is of no use because of its limitation in SOQL. We can definitely build our solution with Wrapper Class here and is more practical use case and solution approach if records are relatively large(>2000).
  • Using ID Field: As we all know, ID fields are indexed fields and are auto-generated in some random fashion. But they have a unique property, each time an ID is generated and linked with record, the ASCII value is greater than all previously generated ID. We can simply use the last record ID in the table and retrieve the next lot of fresh records with ID > retrieved last record ID. This gives a true sense of having infinite loading even if the records are greater than 50,000.(A limitation otherwise in SOQL query rows limit).For example: 
    SELECT Id, Name, AccountNumber, Phone, CreatedDate FROM Account where id > 'LAST RECORD ID IN TABLE' LIMIT 20

    This query will return new set of 20 records followed by previous record id in the Account Table. 

Infinite Loading Implementation: For learning, we will focus on how do we use OFFSET in implementing infinite loading. The other two are then self explainable if we are good with the first and understand thoroughly. 
Let's jump straight to the solution. The code itself is self explanatory, so should be fairly quick to crack on.
  1. Create an Apex Class RelatedListController: The purpose of this class is to fetch the records on request basis. Each time a new request(APEX call from LWC with onloadmore event) is made, it returns the next 20 records from the offSetCount passed in to the getRecords method.
    public class RelatedListController {
        @AuraEnabled
        public static List<Account> getRecords(Integer offSetCount) {
            return [SELECT Id, Name, AccountNumber, Phone, CreatedDate FROM Account LIMIT 20 OFFSET :offSetCount];
        }
    }
  2. Create a LWC component customDatatableDemo: Create LWC component customDatatableDemo as given below.
    i. customDatatableDemo.html: To enable infinite scrolling, specify enable-infinite-loading and provide an event handler using onloadmore on lightning-datatable. By default, data loading is triggered when you scroll down to 20px from the bottom of the table, but the offset can be changed using the load-more-offset attribute.
    <template>
        <lightning-card>
            <div class="slds-var-p-around_small">
                <div>
                    <span
                        class="slds-form-element__label slds-text-title_bold slds-align_absolute-center">
                        Infinite Loading Example</span>
                </div>
                <div>
                    <span class="slds-form-element__label slds-text-title_bold">
                        Total Records: {totalNumberOfRows}</span>
                </div>
                <div>
                    <span class="slds-form-element__label slds-text-title_bold">
                        Displayed Records: {data.length}</span>
                </div>
                <div class="slds-box" style="height: 400px;">
                    <lightning-datatable key-field="Id" data={data} columns={columns} load-more-offset="20"
                        onloadmore={handleLoadMore} enable-infinite-loading hide-checkbox-column show-row-number-column>
                    </lightning-datatable>
                </div>
                {loadMoreStatus}
            </div>
        </lightning-card>
    </template>
    ii. customDatatableDemo.js: The onloadmore event handler retrieves more data when you scroll to the bottom of the table until there are no more data to load. To display a spinner while data is being loaded, set the isLoading property to true.
    import { LightningElement } from 'lwc';
    import { NavigationMixin } from 'lightning/navigation';
    import getRecords from '@salesforce/apex/RelatedListController.getRecords';
    
    const columns = [
        { label: 'Account Name', fieldName: 'linkAccount', type: 'url',
            typeAttributes: {
                label: { fieldName: 'Name' },
                target: '_blank'
            } 
        },
        { label: 'Account Number', fieldName: 'AccountNumber', type: 'text'},
        { label: 'Phone', fieldName: 'Phone', type: 'text'},
        { label: 'Created Date', fieldName: 'CreatedDate', type: 'text'}
    ];
    
    export default class CustomDatatableDemo extends NavigationMixin( LightningElement ) {
        columns = columns;
        data = [];
        error;
        totalNumberOfRows = 200; // stop the infinite load after this threshold count
        // offSetCount to send to apex to get the subsequent result. 0 in offSetCount signifies for the initial load of records on component load.
        offSetCount = 0;
        loadMoreStatus;
        targetDatatable; // capture the loadmore event to fetch data and stop infinite loading
    
        connectedCallback() {
            //Get initial chunk of data with offset set at 0
            this.getRecords();
        }
    
        getRecords() {
            getRecords({offSetCount : this.offSetCount})
                .then(result => {
                    // Returned result if from sobject and can't be extended so objectifying the result to make it extensible
                    result = JSON.parse(JSON.stringify(result));
                    result.forEach(record => {
                        record.linkAccount = '/' + record.Id;
                    });
                    this.data = [...this.data, ...result];
                    this.error = undefined;
                    this.loadMoreStatus = '';
                    if (this.targetDatatable && this.data.length >= this.totalNumberOfRows) {
                        //stop Infinite Loading when threshold is reached
                        this.targetDatatable.enableInfiniteLoading = false;
                        //Display "No more data to load" when threshold is reached
                        this.loadMoreStatus = 'No more data to load';
                    }
                    //Disable a spinner to signal that data has been loaded
                    if (this.targetDatatable) this.targetDatatable.isLoading = false;
                })
                .catch(error => {
                    this.error = error;
                    this.data = undefined;
                    console.log('error : ' + JSON.stringify(this.error));
                });
        }
    
        // Event to handle onloadmore on lightning datatable markup
        handleLoadMore(event) {
            event.preventDefault();
            // increase the offset count by 20 on every loadmore event
            this.offSetCount = this.offSetCount + 20;
            //Display a spinner to signal that data is being loaded
            event.target.isLoading = true;
            //Set the onloadmore event taraget to make it visible to imperative call response to apex.
            this.targetDatatable = event.target;
            //Display "Loading" when more data is being loaded
            this.loadMoreStatus = 'Loading';
            // Get new set of records and append to this.data
            this.getRecords();
        }
    }
    While this example uses a fixed number to denote the total number of rows, you can also use the SOQL SELECT syntax with the COUNT() function to return the number of rows in the object in your Apex controller. Then, set the result on the totalNumberOfRows attribute during initialization.
    Note: I have used here the imperative method to call APEX. You can also go with wire function call to APEX.
    iii. customDatatableDemo.js-meta.xml: 
    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>51.0</apiVersion>
        <isExposed>true</isExposed>
        <targets>
            <target>lightning__Tab</target>
        </targets>
    </LightningComponentBundle>
  3. Create a tab of the component created in step 2 to test our infinite loading in lighitning-datatable in LWC.

Conclusion:

Now as have understood the requirement and implemented the solution, one thing to keep in mind is that when we define the height of lightning-datatable container div element in LWC html file and with enable-infinite-loading, the table will try to load more records if the available height of the lightning-datatable thus rendered with the records doesn't match to the given div component height. Ideally the rendered lightning-datatable height should be greater than the container div element. Think of assigning the height of container div dynamically if you are not sure of records being flooded to lightning-datatable to display.


If you like this blog content and find inciteful, please comment and let me know. 

Comments

  1. This won't help in loading more than 2000 records

    ReplyDelete
  2. Can you give some more details about the method of using Wrapper Class? Thanks.

    ReplyDelete
  3. Would be nice if you could share more about the methods for loading 2,000+ records. Also would be nice to see the writing/code copied from the lightning-datatable documentation (https://developer.salesforce.com/docs/component-library/bundle/lightning-datatable/documentation) distinguished somehow from your original writing/code in this article.

    ReplyDelete
  4. I am facing a weird issue which might actually be there for everyone too. The infinite scrolling works fine and fresh data loads, but everytime the new data loads the vertical scroll bar jumps to the mid of the table. SO lets say if I scrolled to record number 50 and more data loads then I go back to somewhere around record number 25. Might not be a good UX.

    ReplyDelete

Post a Comment