Skip to main content

How to Persist Selected Rows While Filtering a Salesforce LWC Datatable

In plain words: The standard Salesforce <lightning-datatable> has a major quirk: if a user selects a few rows, types a word into a search bar to filter the list, and then clears the search, their original selections disappear! To fix this, you must write custom JavaScript to temporarily store the IDs of selected rows that are "hidden" by the filter, and merge them back when the filter is cleared.

Displaying data in a tabular format is a standard requirement for almost any enterprise application. In Salesforce Lightning Web Components (LWC), the lightning-datatable component makes this easy. However, combining row selection with dynamic front-end filtering introduces a frustrating state-management problem.

Because filtering replaces the data bound to the datatable, any previously selected records that no longer match the search term are "forgotten" by the component. Let's walk through how to build a smart, stateful datatable that remembers row selections no matter how many times the user filters the data.

Step 1: Set Up the Project

First, create a new Lightning Web Component in your Salesforce DX project using the modern Salesforce CLI.

sf lightning generate component -n persistentDatatable -d force-app/main/default/lwc

Step 2: The HTML Template

Open the persistentDatatable.html file. We need two main elements: a lightning-input to act as our search bar, and the lightning-datatable to display the filtered results. Crucially, we must bind the selected-rows attribute to a JavaScript array.

<template>
    <lightning-card title="Smart Datatable Selection" icon-name="standard:account">
        
        <!-- Search Bar -->
        <div class="slds-m-around_medium">
            <lightning-input 
                type="search" 
                label="Filter Accounts" 
                onchange={handleSearch}>
            </lightning-input>
        </div>

        <!-- Datatable -->
        <div class="slds-m-around_medium">
            <lightning-datatable
                key-field="Id"
                data={filteredData}
                columns={columns}
                selected-rows={selectedRowIds}
                onrowselection={handleRowSelection}>
            </lightning-datatable>
        </div>

    </lightning-card>
</template>

Step 3: The JavaScript Logic (State Management)

This is where the heavy lifting happens. Open persistentDatatable.js. We need to maintain a "master list" of selected IDs. Every time the user checks or unchecks a box, we merge the visible selections with the hidden selections.

import { LightningElement, track, wire } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts'; // Example Apex Method

export default class PersistentDatatable extends LightningElement {
    
    @track fullData = [];       // Holds all records from the database
    @track filteredData = [];   // Holds only the records matching the search
    @track selectedRowIds = []; // Master list of selected IDs

    columns = [
        { label: 'Name', fieldName: 'Name' },
        { label: 'Industry', fieldName: 'Industry' }
    ];

    // 1. Fetch data from Apex
    @wire(getAccounts)
    wiredData({ error, data }) {
        if (data) {
            this.fullData = data;
            this.filteredData = data; // Initially, filtered data is all data
        } else if (error) {
            console.error('Error fetching data:', error);
        }
    }

    // 2. Handle the Search Input
    handleSearch(event) {
        const searchTerm = event.target.value.toLowerCase();
        
        // Filter the full dataset
        this.filteredData = this.fullData.filter(record => {
            return record.Name.toLowerCase().includes(searchTerm);
        });
    }

    // 3. The Magic: Handling Row Selection
    handleRowSelection(event) {
        // Step A: Get the IDs of the rows currently selected on the screen
        const visibleSelectedIds = event.detail.selectedRows.map(row => row.Id);
        
        // Step B: Get the IDs of ALL rows currently visible on the screen
        const visibleRowIds = this.filteredData.map(row => row.Id);

        // Step C: Identify previously selected rows that are currently hidden by the filter
        const hiddenSelectedIds = this.selectedRowIds.filter(id => !visibleRowIds.includes(id));

        // Step D: Merge the hidden selections with the visible selections
        this.selectedRowIds = [...hiddenSelectedIds, ...visibleSelectedIds];
    }
}
Developer Trap: Object Mutations
Notice how we map the selected rows down to just their Ids? The selected-rows property expects an array of strings (the key-field values), not an array of objects. Passing full objects into selectedRowIds will cause the checkboxes to fail silently!
360 Card: The Logic Breakdown
If you have 10 records and select 2.
  • Filter applied: 5 records show. The 2 selected records disappear.
  • Hidden Selections: Our code realizes the 2 selected IDs are no longer in the visibleRowIds array, so it saves them in a safe place (hiddenSelectedIds).
  • New Selections: The user selects 1 more record from the filtered 5.
  • The Merge: The code combines the 2 hidden IDs with the 1 visible ID, resulting in an accurate master array of 3 selections.
Core Takeaway: To create a stateful LWC datatable, you must decouple the data you are displaying from the list of selected IDs, and manually merge them every time the onrowselection event fires.

Conclusion

While the standard Lightning Datatable does not handle persistent selections out-of-the-box during filtering, implementing your own state management in JavaScript is highly rewarding. By understanding array filtering and the spread operator (...), you can provide a seamless, robust user experience that doesn't unexpectedly delete your users' hard work.

Happy coding!