How to Persist Row Selection Across Server-Side Pagination in Angular
When using server-side pagination in Angular tables (Angular Material, ngx-datatable, or custom implementations), row selection usually breaks as soon as you:
- Change page
- Apply filters
- Refresh the dataset
- Re-fetch data from the backend
This is not a UI problem.
It is a state architecture problem.
If you need a production-ready persistent selection solution,
you can get the full package here:
https://rawnessg.gumroad.com/l/gckgiz
The Real Issue
With server-side pagination, your table only renders a slice of the dataset.
Every page change replaces the row array:
this.rows = response.items;
If selection is stored inside each row (e.g. row.selected = true), it disappears as soon as new data arrives.
The mistake is coupling selection state to rendered rows.
UI state ≠ Application state
Selection is domain state.
It must live outside the table component.
The Correct Pattern: Single Source of Truth
Selection must be stored independently from the rendered dataset.
Use a Set of stable identifiers:
selectedIds = new Set<string>();
Why a Set?
- O(1) lookup
- No duplicates
- Clean add/remove semantics
- Stable across re-renders
- Selection should always rely on stable IDs, never object references.
- Rendering Rows Properly
When a new page is fetched:
this.rows = response.items;
Do not inject selection into the row model.
Instead, compute it:
isSelected(row: RowModel): boolean {
return this.selectedIds.has(row.id);
}
Template:
<input
type="checkbox"
[checked]="isSelected(row)"
(change)="toggle(row)"
/>
The table becomes a pure projection of state.
It renders selection — it does not own it.
Toggle Logic
toggle(row: RowModel): void {
if (this.selectedIds.has(row.id)) {
this.selectedIds.delete(row.id);
} else {
this.selectedIds.add(row.id);
}
}
No mutation of row objects.
No dependency on pagination logic.
Pagination and Filtering
When:
- Page changes
- Filters change
- Dataset refreshes You simply fetch new data.
Selection persists because it is independent from the currently rendered page slice.
This enables:
- Cross-page selection
- Selection across filters
- Stable bulk operations
Select All (Current Page)
selectAllCurrentPage(rows: RowModel[]): void {
rows.forEach(row => this.selectedIds.add(row.id));
}
deselectAllCurrentPage(rows: RowModel[]): void {
rows.forEach(row => this.selectedIds.delete(row.id));
}
Again — no mutation inside rows.
Where Real-World Complexity Begins
The pattern above is stable and correct.
But production systems introduce additional concerns:
Backend-driven "select all" semantics
Very large datasets
Reconciliation when records disappear
Reactive state management (Signals, NgRx)
Performance constraints
Reusable abstractions across multiple table instances
At that point, selection stops being a small feature and becomes a state management concern with clear architectural boundaries.
Core Takeaway
Cross-page selection is not a UI trick.
It is an architectural decision:
Decouple selection from rendered rows
Store only stable identifiers
Treat selection as application state
Once you do that, pagination and filtering stop breaking your table.
Have you approached this differently in your Angular projects?
I’d be interested in seeing alternative patterns.
If you need a production-ready persistent selection solution,
you can get the full package here:
Top comments (0)