CustomEvent.
Displaying hundreds of records at once on a Salesforce dashboard or custom table creates visual clutter and degrades browser rendering speeds. Rather than duplicating navigation buttons and page calculations across multiple custom components, creating a modular child pagination component ensures clean separation of concerns. The child component manages navigation state, while the parent handles data slicing and server-side queries.
1. Component Architecture & Event Communication
Building a loosely coupled pagination component in LWC relies on standard parent-child reactive communication:
- Parent-to-Child (
@apiProperties): The parent passescurrentPage,totalRecords, andrecordsPerPagedown to the child component. - Internal Child Computations: A getter calculates the total number of page buttons and provides disabled flags for previous/next controls.
- Child-to-Parent (
CustomEvent): When a user selects a page or clicks next/previous, the child dispatches apagechangeevent containing the new page index inevent.detail.
- Public Inputs:
@api currentPage,@api totalRecords,@api recordsPerPage. - Event Dispatch:
this.dispatchEvent(new CustomEvent('pagechange', { detail: pageNumber })). - Styling Standard: Salesforce Lightning Design System (SLDS) button groups (
<lightning-button-group>). - Data Strategy: Client-side array slicing (
Array.slice()) for ≤ 2,000 records; Server-side SOQL pagination for enterprise data volumes.
2. Implementing the Child Pagination Component
The updated child component below includes explicit "Previous" and "Next" controls alongside individual page buttons, ensuring smooth navigation even on small mobile screens.
paginationControl.html)
<template>
<div class="slds-align_absolute-center slds-m-top_medium">
<lightning-button-group>
<!-- Previous Button -->
<lightning-button
label="Previous"
icon-name="utility:chevronleft"
disabled={isFirstPage}
onclick={handlePrevious}>
</lightning-button>
<!-- Numbered Page Buttons -->
<template for:each={pages} for:item="page">
<lightning-button
key={page}
label={page}
data-page={page}
variant={page.variant}
onclick={handlePageClick}>
</lightning-button>
</template>
<!-- Next Button -->
<lightning-button
label="Next"
icon-name="utility:chevronright"
icon-position="right"
disabled={isLastPage}
onclick={handleNext}>
</lightning-button>
</lightning-button-group>
</div>
</template>
paginationControl.js)
import { LightningElement, api } from 'lwc';
export default class PaginationControl extends LightningElement {
@api currentPage = 1;
@api totalRecords = 0;
@api recordsPerPage = 10;
get totalPages() {
if (!this.totalRecords || !this.recordsPerPage) {
return 1;
}
return Math.ceil(this.totalRecords / this.recordsPerPage);
}
get pages() {
const pageList = [];
for (let i = 1; i <= this.totalPages; i++) {
pageList.push({
label: String(i),
number: i,
variant: this.currentPage === i ? 'brand' : 'neutral'
});
}
return pageList;
}
get isFirstPage() {
return this.currentPage <= 1;
}
get isLastPage() {
return this.currentPage >= this.totalPages;
}
handlePageClick(event) {
const selectedPage = parseInt(event.target.label, 10);
this.notifyParent(selectedPage);
}
handlePrevious() {
if (!this.isFirstPage) {
this.notifyParent(this.currentPage - 1);
}
}
handleNext() {
if (!this.isLastPage) {
this.notifyParent(this.currentPage + 1);
}
}
notifyParent(targetPage) {
if (targetPage !== this.currentPage) {
this.dispatchEvent(
new CustomEvent('pagechange', {
detail: targetPage
})
);
}
}
}
3. Consuming the Pagination Component in a Parent View
The parent component queries or holds the master dataset, passes metrics down to the child, and slices the records to show only the current page view.
accountListContainer.html)
<template>
<lightning-card title="Enterprise Accounts" icon-name="standard:account">
<div class="slds-p-around_medium">
<!-- Data Table showing current sliced page records -->
<lightning-datatable
key-field="id"
data={displayedAccounts}
columns={columns}
hide-checkbox-column>
</lightning-datatable>
<!-- Embedded Reusable Pagination Component -->
<c-pagination-control
current-page={currentPage}
total-records={allAccounts.length}
records-per-page={pageSize}
onpagechange={handlePageChange}>
</c-pagination-control>
</div>
</lightning-card>
</template>
accountListContainer.js)
import { LightningElement, track } from 'lwc';
const COLUMNS = [
{ label: 'Account Name', fieldName: 'Name', type: 'text' },
{ label: 'Industry', fieldName: 'Industry', type: 'text' },
{ label: 'Annual Revenue', fieldName: 'AnnualRevenue', type: 'currency' }
];
export default class AccountListContainer extends LightningElement {
columns = COLUMNS;
@track allAccounts = [];
@track displayedAccounts = [];
currentPage = 1;
pageSize = 5;
connectedCallback() {
this.loadMockData();
this.updateDisplayedRecords();
}
loadMockData() {
const mockList = [];
for (let i = 1; i <= 23; i++) {
mockList.push({
id: `ACC-${i}`,
Name: `Global Tech Corp ${i}`,
Industry: i % 2 === 0 ? 'Technology' : 'Finance',
AnnualRevenue: 500000 * i
});
}
this.allAccounts = mockList;
}
handlePageChange(event) {
this.currentPage = event.detail;
this.updateDisplayedRecords();
}
updateDisplayedRecords() {
const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = startIndex + this.pageSize;
this.displayedAccounts = this.allAccounts.slice(startIndex, endIndex);
}
}
4. Common Traps & Performance Best Practices
If a dataset contains 5,000 records with a page size of 10, generating an array of 500 buttons will overflow the card container and crash mobile rendering. For large datasets, truncate the visible page range (e.g., render page 1, 2, 3... next/prev buttons) or implement server-side keyset pagination.
slice(start, end)) in the parent.
- Always Parse Strings to Integers: DOM element attributes passed via dataset or label can return as strings. Always wrap numbers in
parseInt(val, 10)to avoid string concatenation arithmetic bugs (e.g.,'1' + 1 = '11'). - Client-Side Slicing vs. Server-Side SOQL: For datasets under 1,000 records, load all rows into memory and slice client-side with
Array.slice()for instant response times. For larger datasets, make parameterized Apex queries on eachpagechangeevent. - Reset Page on Search Filter Changes: If the parent component filters records by search keyword, always reset
this.currentPage = 1to prevent showing an out-of-range empty page.
Summary
Creating a standalone, reusable pagination component streamlines UI development across your Salesforce Lightning applications. By leveraging clean @api properties, standard CustomEvents, and client-side record slicing, developers can deliver lightning-fast, intuitive data navigation across desktop and mobile devices.