Skip to main content

Implementing Batch Selection and Delete in LWC Tables

Batch operations on table data -- select multiple rows, then act on the selection -- are a standard enterprise UI pattern. This article covers implementing multi-select checkboxes with a select-all control, a delete mode state machine, and the confirmation step before execution.

Using Set for Selection State

A Set is the ideal data structure for tracking selected row IDs. It provides O(1) performance for add, delete, and has operations, compared to O(n) for array-based approaches.

import { LightningElement, track } from "lwc";
import deleteRecords from "@salesforce/apex/RecordController.deleteRecords";
import { ShowToastEvent } from "lightning/platformShowToastEvent";

export default class BatchDeleteTable extends LightningElement {
@track items = [];
_selectedForDelete = new Set();
isDeleteMode = false;
isProcessing = false;

get selectedCount() {
return this._selectedForDelete.size;
}

get hasSelection() {
return this._selectedForDelete.size > 0;
}
}

Row Checkbox Handling

Each row renders a checkbox when delete mode is active. The checkbox state is driven by whether the row's ID exists in the selection Set.

<template>
<table class="slds-table slds-table_bordered">
<thead>
<tr>
<template if:true={isDeleteMode}>
<th class="slds-cell-shrink">
<lightning-input
type="checkbox"
checked={isAllSelected}
onchange={handleSelectAll}
label="Select All"
variant="label-hidden"
></lightning-input>
</th>
</template>
<th>Name</th>
<th>Status</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<template for:each={displayItems} for:item="row">
<tr key={row.Id} class={row.rowClass}>
<template if:true={isDeleteMode}>
<td class="slds-cell-shrink">
<lightning-input
type="checkbox"
checked={row.isSelected}
data-id={row.Id}
onchange={handleRowSelect}
label={row.Name}
variant="label-hidden"
></lightning-input>
</td>
</template>
<td>{row.Name}</td>
<td>{row.Status__c}</td>
<td>
<lightning-formatted-number
value={row.Amount__c}
style="currency"
currency-code="USD"
></lightning-formatted-number>
</td>
</tr>
</template>
</tbody>
</table>
</template>

Computing Display Items with Selection State

The display getter merges selection state into each row object:

get displayItems() {
return this.items.map((item) => ({
...item,
isSelected: this._selectedForDelete.has(item.Id),
rowClass: this._selectedForDelete.has(item.Id)
? "slds-is-selected"
: "",
}));
}

Toggle Individual Rows

handleRowSelect(event) {
const recordId = event.target.dataset.id;
if (event.target.checked) {
this._selectedForDelete.add(recordId);
} else {
this._selectedForDelete.delete(recordId);
}
// Trigger reactivity
this.items = [...this.items];
}

Since Set mutations do not trigger LWC reactivity, reassigning this.items to a new array reference forces the displayItems getter to re-evaluate.

Select All (Visible Items Only)

The select-all checkbox should operate only on the currently visible (filtered) items, not the full dataset. This prevents accidentally selecting rows the user cannot see.

get visibleIds() {
return this.displayItems.map((item) => item.Id);
}

get isAllSelected() {
if (this.displayItems.length === 0) return false;
return this.visibleIds.every((id) => this._selectedForDelete.has(id));
}

handleSelectAll(event) {
if (event.target.checked) {
this.visibleIds.forEach((id) => this._selectedForDelete.add(id));
} else {
this.visibleIds.forEach((id) => this._selectedForDelete.delete(id));
}
this.items = [...this.items];
}

Delete Mode State Machine

The delete workflow follows three states:

  1. Normal mode -- Standard table view with regular action buttons
  2. Delete mode -- Checkboxes appear, action bar shows "Delete Selected" and "Cancel"
  3. Confirmation -- A prompt verifying the user's intent before executing
handleEnterDeleteMode() {
this.isDeleteMode = true;
this._selectedForDelete.clear();
this.items = [...this.items];
}

handleCancelDeleteMode() {
this.isDeleteMode = false;
this._selectedForDelete.clear();
this.items = [...this.items];
}

handleDeleteSelected() {
// Show confirmation before proceeding
this.showConfirmation = true;
}

async handleConfirmDelete() {
this.isProcessing = true;
this.showConfirmation = false;
try {
const idsToDelete = Array.from(this._selectedForDelete);
await deleteRecords({ recordIds: idsToDelete });

// Remove deleted items from local state
this.items = this.items.filter(
(item) => !this._selectedForDelete.has(item.Id)
);
this._selectedForDelete.clear();
this.isDeleteMode = false;

this.dispatchEvent(
new ShowToastEvent({
title: "Success",
message: `${idsToDelete.length} record(s) deleted.`,
variant: "success",
})
);
} catch (error) {
this.dispatchEvent(
new ShowToastEvent({
title: "Error",
message: error.body?.message || "Delete failed.",
variant: "error",
})
);
} finally {
this.isProcessing = false;
}
}

Action Bar Conditional Rendering

Toggle between normal and delete-mode action bars:

<div class="slds-m-bottom_small">
<template if:false={isDeleteMode}>
<lightning-button
label="Delete Mode"
variant="destructive"
onclick={handleEnterDeleteMode}
icon-name="utility:delete"
></lightning-button>
</template>
<template if:true={isDeleteMode}>
<lightning-button
label={deleteButtonLabel}
variant="destructive"
onclick={handleDeleteSelected}
disabled={deleteDisabled}
></lightning-button>
<lightning-button
label="Cancel"
onclick={handleCancelDeleteMode}
class="slds-m-left_small"
></lightning-button>
</template>
</div>
get deleteButtonLabel() {
return `Delete Selected (${this.selectedCount})`;
}

get deleteDisabled() {
return !this.hasSelection || this.isProcessing;
}

State Reset on Mode Exit

Always clear the selection set when exiting delete mode, whether through cancellation or successful deletion. This prevents stale selections from carrying over if the user re-enters delete mode later.

The Set-based selection pattern combined with a clear state machine makes batch operations predictable and performant, even with hundreds of visible rows.