<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rawness</title>
    <description>The latest articles on DEV Community by Rawness (@rawness_).</description>
    <link>https://dev.to/rawness_</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3768524%2F3a7a6594-7260-4704-a626-517156f5c3ce.png</url>
      <title>DEV Community: Rawness</title>
      <link>https://dev.to/rawness_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rawness_"/>
    <language>en</language>
    <item>
      <title>Angular: Keep Table Selection Across Pages (Without Losing Your Mind)</title>
      <dc:creator>Rawness</dc:creator>
      <pubDate>Thu, 12 Feb 2026 10:42:15 +0000</pubDate>
      <link>https://dev.to/rawness_/angular-keep-table-selection-across-pages-without-losing-your-mind-48me</link>
      <guid>https://dev.to/rawness_/angular-keep-table-selection-across-pages-without-losing-your-mind-48me</guid>
      <description>&lt;p&gt;&lt;strong&gt;How to Persist Row Selection Across Server-Side Pagination in Angular&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When using server-side pagination in Angular tables (Angular Material, ngx-datatable, or custom implementations), row selection usually breaks as soon as you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change page&lt;/li&gt;
&lt;li&gt;Apply filters&lt;/li&gt;
&lt;li&gt;Refresh the dataset&lt;/li&gt;
&lt;li&gt;Re-fetch data from the backend&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not a UI problem.&lt;br&gt;
It is a state architecture problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;If you need a production-ready persistent selection solution,&lt;br&gt;
you can get the full package here:&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rawnessg.gumroad.com/l/gckgiz" rel="noopener noreferrer"&gt;https://rawnessg.gumroad.com/l/gckgiz&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Real Issue&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With server-side pagination, your table only renders a slice of the dataset.&lt;/p&gt;

&lt;p&gt;Every page change replaces the row array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;this.rows = response.items;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If selection is stored inside each row (e.g. row.selected = true), it disappears as soon as new data arrives.&lt;/p&gt;

&lt;p&gt;The mistake is coupling selection state to rendered rows.&lt;/p&gt;

&lt;p&gt;UI state ≠ Application state&lt;/p&gt;

&lt;p&gt;Selection is domain state.&lt;br&gt;
It must live outside the table component.&lt;/p&gt;

&lt;p&gt;The Correct Pattern: Single Source of Truth&lt;/p&gt;

&lt;p&gt;Selection must be stored independently from the rendered dataset.&lt;/p&gt;

&lt;p&gt;Use a Set of stable identifiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selectedIds = new Set&amp;lt;string&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why a Set?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;O(1) lookup&lt;/li&gt;
&lt;li&gt;No duplicates&lt;/li&gt;
&lt;li&gt;Clean add/remove semantics&lt;/li&gt;
&lt;li&gt;Stable across re-renders&lt;/li&gt;
&lt;li&gt;Selection should always rely on stable IDs, never object references.&lt;/li&gt;
&lt;li&gt;Rendering Rows Properly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a new page is fetched:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;this.rows = response.items;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Do not inject selection into the row model.&lt;/p&gt;

&lt;p&gt;Instead, compute it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
isSelected(row: RowModel): boolean {
  return this.selectedIds.has(row.id);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Template:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;input&lt;br&gt;
  type="checkbox"&lt;br&gt;
  [checked]="isSelected(row)"&lt;br&gt;
  (change)="toggle(row)"&lt;br&gt;
/&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The table becomes a pure projection of state.&lt;/p&gt;

&lt;p&gt;It renders selection — it does not own it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Toggle Logic&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;toggle(row: RowModel): void {
  if (this.selectedIds.has(row.id)) {
    this.selectedIds.delete(row.id);
  } else {
    this.selectedIds.add(row.id);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No mutation of row objects.&lt;br&gt;
No dependency on pagination logic.&lt;/p&gt;

&lt;p&gt;Pagination and Filtering&lt;/p&gt;

&lt;p&gt;When:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Page changes&lt;/li&gt;
&lt;li&gt;Filters change&lt;/li&gt;
&lt;li&gt;Dataset refreshes
You simply fetch new data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Selection persists because it is independent from the currently rendered page slice.&lt;/p&gt;

&lt;p&gt;This enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-page selection&lt;/li&gt;
&lt;li&gt;Selection across filters&lt;/li&gt;
&lt;li&gt;Stable bulk operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Select All (Current Page)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selectAllCurrentPage(rows: RowModel[]): void {
  rows.forEach(row =&amp;gt; this.selectedIds.add(row.id));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deselectAllCurrentPage(rows: RowModel[]): void {
  rows.forEach(row =&amp;gt; this.selectedIds.delete(row.id));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again — no mutation inside rows.&lt;/p&gt;

&lt;p&gt;Where Real-World Complexity Begins&lt;/p&gt;

&lt;p&gt;The pattern above is stable and correct.&lt;br&gt;
But production systems introduce additional concerns:&lt;/p&gt;

&lt;p&gt;Backend-driven "select all" semantics&lt;/p&gt;

&lt;p&gt;Very large datasets&lt;/p&gt;

&lt;p&gt;Reconciliation when records disappear&lt;/p&gt;

&lt;p&gt;Reactive state management (Signals, NgRx)&lt;/p&gt;

&lt;p&gt;Performance constraints&lt;/p&gt;

&lt;p&gt;Reusable abstractions across multiple table instances&lt;/p&gt;

&lt;p&gt;At that point, selection stops being a small feature and becomes a state management concern with clear architectural boundaries.&lt;/p&gt;

&lt;p&gt;Core Takeaway&lt;/p&gt;

&lt;p&gt;Cross-page selection is not a UI trick.&lt;/p&gt;

&lt;p&gt;It is an architectural decision:&lt;/p&gt;

&lt;p&gt;Decouple selection from rendered rows&lt;/p&gt;

&lt;p&gt;Store only stable identifiers&lt;/p&gt;

&lt;p&gt;Treat selection as application state&lt;/p&gt;

&lt;p&gt;Once you do that, pagination and filtering stop breaking your table.&lt;/p&gt;

&lt;p&gt;Have you approached this differently in your Angular projects?&lt;br&gt;
I’d be interested in seeing alternative patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;If you need a production-ready persistent selection solution,&lt;br&gt;
you can get the full package here:&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rawnessg.gumroad.com/l/gckgiz" rel="noopener noreferrer"&gt;https://rawnessg.gumroad.com/l/gckgiz&lt;/a&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>architecture</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
