Skip to main content

Client-Side Search, Filtering, and Sorting in Lightning Web Components

When working with datasets under approximately 500 records, client-side data operations in LWC deliver near-instant responsiveness. Beyond that threshold, consider server-side approaches. This article covers the core patterns for search, filtering, and sorting that compose cleanly together.

The Computed Getter Pattern

LWC's reactive system re-evaluates getter methods whenever any tracked property they reference changes. This makes getters the natural place to combine search, filter, and sort logic into a single derived collection.

import { LightningElement, track, wire } from "lwc";
import getItems from "@salesforce/apex/ItemController.getItems";

export default class ItemList extends LightningElement {
@track allItems = [];
searchTerm = "";
selectedStatus = "";
showActiveOnly = false;
sortField = "Name";
sortDirection = "asc";

@wire(getItems)
wiredItems({ data }) {
if (data) {
this.allItems = data;
}
}

get filteredItems() {
let result = [...this.allItems];

// Text search
if (this.searchTerm) {
const term = this.searchTerm.toLowerCase();
result = result.filter(
(item) =>
item.Name.toLowerCase().includes(term) ||
(item.Description__c || "").toLowerCase().includes(term)
);
}

// Picklist filter
if (this.selectedStatus) {
result = result.filter(
(item) => item.Status__c === this.selectedStatus
);
}

// Checkbox filter
if (this.showActiveOnly) {
result = result.filter((item) => item.IsActive__c === true);
}

// Sort
const direction = this.sortDirection === "asc" ? 1 : -1;
const field = this.sortField;
result.sort((a, b) => {
const valA = a[field] || "";
const valB = b[field] || "";
if (valA < valB) return -1 * direction;
if (valA > valB) return 1 * direction;
return 0;
});

return result;
}
}

The template iterates over filteredItems instead of allItems. Every time searchTerm, selectedStatus, showActiveOnly, or the sort properties change, the getter re-evaluates automatically.

Debounced Search Input

Firing a filter pass on every keystroke is wasteful. A debounce delay of 200-300ms waits for the user to pause typing before updating the search term.

_searchTimeout;

handleSearchChange(event) {
const value = event.target.value;
clearTimeout(this._searchTimeout);
this._searchTimeout = setTimeout(() => {
this.searchTerm = value;
}, 250);
}
<lightning-input
label="Search"
type="search"
onchange={handleSearchChange}
placeholder="Search by name or description..."
></lightning-input>

Multi-Criteria Filter Controls

Combine different filter types in the same UI. Each control updates a single tracked property, and the computed getter applies all of them.

<lightning-combobox
label="Status"
value={selectedStatus}
options={statusOptions}
onchange={handleStatusChange}
></lightning-combobox>

<lightning-input
type="checkbox"
label="Active Only"
checked={showActiveOnly}
onchange={handleActiveToggle}
></lightning-input>
handleStatusChange(event) {
this.selectedStatus = event.detail.value;
}

handleActiveToggle(event) {
this.showActiveOnly = event.target.checked;
}

Using Set for Efficient Filtering

When filtering against a collection of selected values (e.g., multi-select checkboxes), a Set provides O(1) lookup compared to Array.includes() which is O(n).

_selectedCategories = new Set();

handleCategoryToggle(event) {
const value = event.target.dataset.value;
if (this._selectedCategories.has(value)) {
this._selectedCategories.delete(value);
} else {
this._selectedCategories.add(value);
}
// Trigger reactivity
this.filterVersion = Date.now();
}

get filteredItems() {
let result = [...this.allItems];
if (this._selectedCategories.size > 0) {
result = result.filter((item) =>
this._selectedCategories.has(item.Category__c)
);
}
return result;
}

Note that Set mutations do not trigger LWC reactivity on their own. The filterVersion property acts as a change signal. Alternatively, reassign the set: this._selectedCategories = new Set(this._selectedCategories).

Sort Direction Toggle

Column header clicks cycle through ascending and descending sort:

handleSort(event) {
const field = event.currentTarget.dataset.field;
if (this.sortField === field) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortField = field;
this.sortDirection = 'asc';
}
}

Performance Considerations

Record CountRecommended Approach
< 200Client-side filtering with no concerns
200 - 500Client-side with debounced search
500 - 2,000Hybrid: initial server query, client-side refinement
> 2,000Server-side SOQL with OFFSET/LIMIT

For large datasets, move filter logic into Apex with @AuraEnabled(cacheable=true) and dynamic SOQL. The wire adapter will cache results for identical parameter sets automatically.

The computed getter pattern keeps all filtering, searching, and sorting logic in one readable location, making it straightforward to add or remove criteria without restructuring the component.