Skip to main content

Reusable Pagination Component in LWC: Client vs. Server-Side Guide

In plain words: A Reusable Pagination Component in Lightning Web Components (LWC) is a standalone UI control that breaks down massive data lists into numbered pages. It calculates total pages dynamically based on record count, highlights the active page button, and communicates page changes to parent components using a standard JavaScript 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 (@api Properties): The parent passes currentPage, totalRecords, and recordsPerPage down 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 a pagechange event containing the new page index in event.detail.
360 LWC Pagination Blueprint:
  • 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.

Step 1: Child HTML Template (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>
Step 2: Child JavaScript Controller (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.

Step 3: Parent HTML Template (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>
Step 4: Parent JavaScript Controller (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

UI Trap: Rendering Unbounded Page Buttons (100+ Pages)
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.
Core Rule: Keep pagination controls completely stateless regarding data schemas. Encapsulate all navigation mathematics in the child and handle record array slicing (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 each pagechange event.
  • Reset Page on Search Filter Changes: If the parent component filters records by search keyword, always reset this.currentPage = 1 to 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.