DEV Community

Rawness
Rawness

Posted on

Angular: Keep Table Selection Across Pages (Without Losing Your Mind)

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:

  1. Change page
  2. Apply filters
  3. Refresh the dataset
  4. 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;
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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);
}

Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode
deselectAllCurrentPage(rows: RowModel[]): void {
  rows.forEach(row => this.selectedIds.delete(row.id));
}
Enter fullscreen mode Exit fullscreen mode

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:

https://rawnessg.gumroad.com/l/gckgiz

Top comments (0)