Lightning Web Component

How to create a Lightning Datatable Pagination

Using a Lightning Datatable Pagination feature will improve the user experience by making navigation across large amounts of records more comfortable and efficient without having to load all of them at once.

Pagination can be implemented using server-side or client-side logic. This article focuses on server-side pagination with LWC and Apex.

How to create a Lightning Datatable Pagination

Lightning Datatable Pagination Example

Here is the whole component that you're looking for. However, we will explain to you step by step how we created it so you can be more familiarized and modify it to your needs.

Lightning Datatable Pagination in LWC

Apex Controller

public with sharing class LightningDatatablePaginationController {

    @AuraEnabled
    public static List<Contact> getContacts(Integer recordsNum, List<Id> idsDisplayed) {
        return [SELECT Id, Firstname, Lastname, Birthdate, Email FROM Contact WHERE Id NOT IN: idsDisplayed LIMIT: recordsNum];
    }
}

Component Structure: HTML

<template>
<template if:true={isLoading}>
    <div class="spinner">
        <lightning-spinner alternative-text="Loading" size="medium"></lightning-spinner>
    </div>
</template>
<template if:false={isLoading}>
    <lightning-datatable 
        key-field="id" 
        data={data} 
        columns={columns}
        show-row-number-column>
    </lightning-datatable>
</template>
<section class="handletable-container">
    <lightning-button-icon icon-name="utility:left" title="Previous" onclick={handlePreviousPage} disabled={disPrev}>
    </lightning-button-icon>
    <span>{currentPage}</span>
    <lightning-button-icon icon-name="utility:right" title="Next" onclick={handleNextPage} disabled={disNext}>
    </lightning-button-icon>
    <lightning-combobox
        label="Page Size"
        class="handletable-combobox"
        value={pageSize}
        options={options}
        onchange={handleChangePageSize}
    ></lightning-combobox>
</section>
</template>

Component Logic: Javascript

import { LightningElement } from 'lwc';
import getContacts from '@salesforce/apex/LightningDatatablePaginationController.getContacts'

const columns = [
    { label: 'Firstname', fieldName: 'FirstName' },
    { label: 'Lastname', fieldName: 'LastName'},
    { label: 'Email', fieldName: 'Email'},
    { label: 'Birthdate', fieldName: 'Birthdate', type: 'date'},
];

const options = [
    { label: 5, value: '5' },
    { label: 10, value: '10' },
    { label: 20, value: '20' },
    { label: 50, value: '50' },
    { label: 100, value: '100' },
]

export default class LightningDatatablePagination extends LightningElement {
    columns = columns;
    options = options;
    data;
    allData = [];
    idsDisplayed = [];
    pageSize = '10';
    currentPage = 1;
    isLoading = false;
    
    get disPrev(){
        if(this.currentPage == 1 || this.isLoading) return true;
        else return false;
    }
    
    firstNext = true;
    fetchContacts(){
        this.isLoading = true;
        this.disNext = true;
        let noData = false;

        getContacts({recordsNum: this.pageSize, idsDisplayed: this.idsDisplayed})
        .then(res => {
            if(this.firstNext) this.data = res;
            if(res.length < this.pageSize) noData = true;
            this.allData.push(res);

            res.forEach(element => {
                if(!this.idsDisplayed.includes(element.Id)) this.idsDisplayed.push(element.Id);
            });
        })
        .catch(err => {
            console.error(err);
        })
        .finally(() => {
            if (!this.firstNext) {
                this.allData = this.divideArray(this.allData.flat(), this.pageSize);
                this.data = this.allData[this.currentPage - 1];
            }
    
            this.firstNext = false;
            this.isLoading = false;
            if(noData) this.disNext = true;
            else this.disNext = false;
        })
    }

    connectedCallback() {
        this.fetchContacts();
    }

    handleNextPage(){
        this.currentPage++;
        if (this.allData[this.currentPage - 1] == undefined || this.allData[this.currentPage - 1].length < this.pageSize) {
            this.fetchContacts();
        }else{
            this.data = this.allData[this.currentPage - 1];
        }
    }

    handlePreviousPage(){
        this.currentPage--;
        this.disNext = false;
        this.data = this.allData[this.currentPage - 1];
    }

    handleChangePageSize(e) {
        this.pageSize = e.target.value;
        this.currentPage = 1;
        
        if(this.allData[this.currentPage - 1].length < this.pageSize){
            this.fetchContacts();
        }else{
            this.allData = this.divideArray(this.allData.flat(), this.pageSize);
            this.data = this.allData[0];
        }
        
        this.disNext = false;
    }
      
    divideArray(arr, size) {
        let result = [];
        let currentPage = [];

        arr.forEach(item => {            
            if(currentPage.length < size) currentPage.push(item);
            else {
                result.push(currentPage);

                currentPage = [];
                currentPage.push(item);
            }
        });
        if(currentPage.length > 0) result.push(currentPage);

        return result;
    }
}

Component Styles: CSS (Optional)

.handletable-container{
    width: 100%;
    height: 4rem;
    background: #f2f2f2;
    padding: 5px 20px;
    display: inline-flex;
    align-items: center;
}

.handletable-container span{
    padding: 0px 10px;
    font-size: 1rem;
}

.handletable-container lightning-combobox{
    width: 5rem;
    right: 0;
    position: absolute;
    margin: 0px 10px;
}

.spinner{
    position: relative;
    background-color: white;
    width: 100%;
    height: 10rem;
}

Lightning Datatable Pagination: Step-by-Step Guide

A quick step-by-step guide about the component so you can be more educated on how it works.

Apex Controller

First, we must create the Apex Controller to get all the data for the lightning datatable. In this case, we send it two variables:

public with sharing class LightningDatatablePaginationController {

    @AuraEnabled
    public static List<Contact> getContacts(Integer recordsNum, List<Id> idsDisplayed) {
        return [SELECT Id, Firstname, Lastname, Birthdate, Email FROM Contact WHERE Id NOT IN: idsDisplayed LIMIT: recordsNum];
    }
}

HTML

On the HTML, we have a few things to explain:

<template>
<!-- CHECK IF COMPONENT IS LOADING -->
<template if:true={isLoading}>
    <div class="spinner">
        <lightning-spinner alternative-text="Loading" size="medium"></lightning-spinner>
    </div>
</template>
<!-- DATATABLE IF THE COMPONENT IT'S NOT LOADING -->
<template if:false={isLoading}>
    <lightning-datatable 
        key-field="id" 
        data={data} 
        columns={columns}
        show-row-number-column>
    </lightning-datatable>
</template>
<!-- -------- -->

<!-- MANAGE THE LIGHTNING DATATABLE -->
<section class="handletable-container">
    <!-- GO NEXT -->
    <lightning-button-icon icon-name="utility:left" title="Previous" onclick={handlePreviousPage} disabled={disPrev}>
    </lightning-button-icon>
    <!-- USER CURRENT PAGE -->
    <span>{currentPage}</span>
    <!-- GO BACK -->
    <lightning-button-icon icon-name="utility:right" title="Next" onclick={handleNextPage} disabled={disNext}>
    </lightning-button-icon>
    <!-- SELECT NUMBER OF RECORDS PER PAGE -->
    <lightning-combobox
        label="Page Size"
        class="handletable-combobox"
        value={pageSize}
        options={options}
        onchange={handleChangePageSize}
    ></lightning-combobox>
</section>
</template>

Javascript

Here, start the most exciting and complex part of the component, the logic of it. Let's start step by step on how it works so you stay aware of the situation.

1. Imports and Columns

Import the necessary modules and define the columns and options for the lightning datatable.

import { LightningElement } from 'lwc';
import getContacts from '@salesforce/apex/LightningDatatablePaginationController.getContacts'

const columns = [
    { label: 'Firstname', fieldName: 'FirstName' },
    { label: 'Lastname', fieldName: 'LastName'},
    { label: 'Email', fieldName: 'Email'},
    { label: 'Birthdate', fieldName: 'Birthdate', type: 'date'},
];

const options = [
    { label: 5, value: '5' },
    { label: 10, value: '10' },
    { label: 20, value: '20' },
    { label: 50, value: '50' },
    { label: 100, value: '100' },
]

2. Properties

Define the class for the component and initialize the necessary properties. Here is a quick description of them:

export default class LightningDatatablePagination extends LightningElement {
    columns = columns;
    options = options;
    data;
    allData = [];
    idsDisplayed = [];
    pageSize = '10';
    currentPage = 1;
    isLoading = false;
    
}

3. Get Data From the Server

Define a method to fetch the data from the server using the getContacts Apex method. This method should consider the current page, page size, and any previously displayed IDs to avoid duplicates.

fetchContacts(){
    this.isLoading = true;
    this.disNext = true;
    let noData = false;

    getContacts({recordsNum: this.pageSize, idsDisplayed: this.idsDisplayed})
    .then(res => {
        if(this.firstNext) this.data = res;
        if(res.length < this.pageSize) noData = true;
        this.allData.push(res);

        res.forEach(element => {
            if(!this.idsDisplayed.includes(element.Id)) this.idsDisplayed.push(element.Id);
        });
    })
    .catch(err => {
        console.error(err);
    })
    .finally(() => {
        if (!this.firstNext) {
            this.allData = this.divideArray(this.allData.flat(), this.pageSize);
            this.data = this.allData[this.currentPage - 1];
        }

        this.firstNext = false;
        this.isLoading = false;
        if(noData) this.disNext = true;
        else this.disNext = false;
    })
}

4. connectedCallback

Call the fetchContacts method in the connectedCallback lifecycle hook to fetch the data when the component is initialized.

connectedCallback() {
    this.fetchContacts();
}

5. Go to the Next Page

Define a method to handle the next page button click. This method should increment the current page and fetch the data if it hasn't been fetched yet.

handleNextPage(){
    this.currentPage++;
    if (this.allData[this.currentPage - 1] == undefined || this.allData[this.currentPage - 1].length < this.pageSize) {
        this.fetchContacts();
    }else{
        this.data = this.allData[this.currentPage - 1];
    }
}

6. Go to the Previous Page

Define a method to handle the previous page button click. This method should decrement the current page and set the data to the appropriate page. Also it must enable the Next button if it was disabled.

handlePreviousPage(){
    this.currentPage--;
    this.disNext = false;
    this.data = this.allData[this.currentPage - 1];
}

7. Divide the Data into Pages

Define a method to divide an array into smaller arrays of a specified size. This method will be used to separate the fetched data into pages.

divideArray(arr, size) {
    let result = [];
    let currentPage = [];

    arr.forEach(item => {            
        if(currentPage.length < size) currentPage.push(item);
        else {
            result.push(currentPage);

            currentPage = [];
            currentPage.push(item);
        }
    });
    if(currentPage.length > 0) result.push(currentPage);

    return result;
}

8. Set Page Size

Define a method to handle changes to the page size dropdown. This method should update the page size and reset the current page to 1.

If the data for the first page still needs to be retrieved, it should fetch it. Otherwise, it should divide the existing data into pages based on the new page size and set it to the first page.

handleChangePageSize(e) {
    this.pageSize = e.target.value;
    this.currentPage = 1;

    if(this.allData[this.currentPage - 1].length < this.pageSize){
        this.fetchContacts();
    }else{
        this.allData = this.divideArray(this.allData.flat(), this.pageSize);
        this.data = this.allData[0];
    }

    this.disNext = false;
}