Displaying large datasets in Salesforce can strain browser performance and exceed Apex governor limits. With the standard lightning-datatable component, you can implement infinite loading using the native onloadmore and enable-infinite-loading attributes, delivering a smooth scrolling experience.
Prerequisites
- A Salesforce Developer Edition, Sandbox, or Scratch Org.
- Salesforce CLI (
sf) installed and authenticated. - Basic understanding of imperative Apex calls and asynchronous JavaScript handling in LWC.
Step 1: Set Up the Server-Side Apex Controller
Create an Apex class named AccountTableController.cls. Use SOQL LIMIT and OFFSET clauses to return records in chunks, and check total count to determine when all records have loaded:
public with sharing class AccountTableController {
public class AccountWrapper {
@AuraEnabled public List<Account> accounts;
@AuraEnabled public Integer totalRecords;
}
@AuraEnabled(cacheable=true)
public static AccountWrapper getAccounts(Integer recordLimit, Integer recordOffset) {
AccountWrapper wrapper = new AccountWrapper();
wrapper.totalRecords = [SELECT COUNT() FROM Account WITH USER_MODE];
wrapper.accounts = [
SELECT Id, Name, Industry, Phone, AnnualRevenue
FROM Account
WITH USER_MODE
ORDER BY Name ASC
LIMIT :recordLimit
OFFSET :recordOffset
];
return wrapper;
}
}
onloadmore handler.
Step 2: Build the LWC Template Markup
In infiniteDataTable.html, place the lightning-datatable inside a container with a fixed height. Bind the onloadmore event and control infinite loading using enable-infinite-loading:
<template>
<lightning-card title="Account Directory (Infinite Scroll)" icon-name="standard:account">
<div class="slds-p-around_medium">
<div style="height: 350px;">
<lightning-datatable
key-field="Id"
data={data}
columns={columns}
enable-infinite-loading={enableInfiniteLoading}
onloadmore={handleLoadMore}
is-loading={isLoading}
hide-checkbox-column>
</lightning-datatable>
</div>
<template lwc:if={loadMoreStatus}>
<p class="slds-text-align_center slds-text-color_weak slds-m-top_small">
{loadMoreStatus}
</p>
</template>
</div>
</lightning-card>
</template>
Step 3: Implement the JavaScript Controller Logic
In infiniteDataTable.js, make an initial imperative Apex call on load and append additional rows to the data array as the user reaches the bottom:
import { LightningElement } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getAccounts from '@salesforce/apex/AccountTableController.getAccounts';
const COLUMNS = [
{ label: 'Account Name', fieldName: 'Name', type: 'text' },
{ label: 'Industry', fieldName: 'Industry', type: 'text' },
{ label: 'Phone', fieldName: 'Phone', type: 'phone' },
{ label: 'Annual Revenue', fieldName: 'AnnualRevenue', type: 'currency' }
];
export default class InfiniteDataTable extends LightningElement {
columns = COLUMNS;
data = [];
recordLimit = 20;
recordOffset = 0;
totalRecords = 0;
isLoading = false;
enableInfiniteLoading = true;
loadMoreStatus = '';
connectedCallback() {
this.fetchRecords();
}
fetchRecords() {
this.isLoading = true;
getAccounts({ recordLimit: this.recordLimit, recordOffset: this.recordOffset })
.then(result => {
this.totalRecords = result.totalRecords;
const newRecords = result.accounts;
// Concatenate new batch to existing dataset
this.data = [...this.data, ...newRecords];
this.recordOffset += this.recordLimit;
// Disable infinite scrolling when all records have been loaded
if (this.data.length >= this.totalRecords) {
this.enableInfiniteLoading = false;
this.loadMoreStatus = `All ${this.totalRecords} records loaded.`;
}
})
.catch(error => {
this.enableInfiniteLoading = false;
this.showToast('Error', error?.body?.message || 'Failed to load records', 'error');
})
.finally(() => {
this.isLoading = false;
});
}
handleLoadMore(event) {
if (this.data.length < this.totalRecords) {
this.fetchRecords();
} else {
this.enableInfiniteLoading = false;
event.target.isLoading = false;
}
}
showToast(title, message, variant) {
this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
}
}
- Native Scroll Event: Always rely on
onloadmorerather than manualwindow.onscrollevent listeners. - Fixed Height Container: A fixed height style (e.g.,
height: 350px;) on the parent wrapper element is required for the internal scrollbar to trigger properly. - SOQL OFFSET Limit: The maximum SOQL
OFFSETsupported by Salesforce is 2,000. For datasets exceeding 2,000 rows, use keyset pagination (sorting by ID or timestamp) instead ofOFFSET.
Step 4: Configure Metadata and Deploy
Update infiniteDataTable.js-meta.xml to make the component available in Lightning App Builder:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__HomePage</target>
</targets>
</LightningComponentBundle>
# Deploy the controller and LWC bundle
sf project deploy start
# Open target org in browser
sf org open
- In Salesforce Setup, navigate to Lightning App Builder.
- Drop
infiniteDataTableonto your page layout, save, and activate. - Scroll to the bottom of the table to verify automated batch retrieval in real time.
onloadmore datatable event with paginated imperative Apex calls provides clean, high-performance lazy loading while keeping browser memory and SOQL limits under control.