Server‑Side MatTable with a Custom DataSource: Pagination, Sorting, and Trade‑offs
MatTable lacks built‑in server‑side pagination or sorting; you must wire MatPaginator and MatSort events to a custom DataSource that fetches only the current page.
15 Apr 2026, 04:30 UTC

MatTable renders fast until the data arrives in the browser. With tens of thousands of rows the table becomes slow to render, memory usage grows, and initial load time dominates UX. The useful takeaway is that Angular Material does not provide built‑in server‑side pagination or sorting. MatPaginator and MatSort only emit events; a custom DataSource or service layer must translate those events into backend requests and feed only the current page to the table.
What MatTable actually gives you
MatTable is a rendering engine. It expects an array‑like source and handles row creation, trackBy optimization, and ARIA attributes such as aria‑rowindex. MatPaginator emits PageEvent with pageIndex, pageSize and length. MatSort emits SortEvent with active column and direction. Neither component knows about HTTP.
From Angular Material v15 onward components are built on Material Design Components for the web, MDC, and use CSS custom properties for theming. That changes internal DOM and class names compared to legacy Material 2. Standalone APIs are the default in recent releases and require Angular 14+.
Wiring server state through a custom DataSource
A supported pattern is a DataSource that implements connect and disconnect and calls a backend on changes. The DataSource owns loading state, page, sort and filter parameters, and exposes a stream the table subscribes to.
Version assumption: this pattern applies to @angular/material v15+ with standalone imports. Verify your version in package.json and compare with release notes for MDC migration and standalone support.
// server-table.datasource.ts
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, finalize, switchMap } from 'rxjs/operators';
export class ServerTableDataSource<T> extends DataSource<T> {
private dataSubject = new BehaviorSubject<T[]>([]);
private loadingSubject = new BehaviorSubject<boolean>(false);
public loading$ = this.loadingSubject.asObservable();
private pageIndex = 0;
private pageSize = 25;
private sortActive = '';
private sortDirection: 'asc'|'desc'|'' = '';
constructor(private fetchFn: (params: any) => Observable<{items:T[]; total:number}>) {
super();
}
connect(_: CollectionViewer): Observable<T[]> {
return this.dataSubject.asObservable();
}
disconnect(_: CollectionViewer): void {
this.dataSubject.complete();
this.loadingSubject.complete();
}
loadPage(pageIndex: number, pageSize: number, sortActive: string, sortDirection: 'asc'|'desc'|'') {
this.loadingSubject.next(true);
this.pageIndex = pageIndex;
this.pageSize = pageSize;
this.sortActive = sortActive;
this.sortDirection = sortDirection;
this.fetchFn({
pageIndex,
pageSize,
sortActive,
sortDirection
}).pipe(
catchError(() => of({items: [], total: 0})),
finalize(() => this.loadingSubject.next(false))
).subscribe(res => {
this.dataSubject.next(res.items);
// total length is handled by the component that owns MatPaginator
});
}
}
The component wires paginator and sort events to loadPage and updates the paginator length from the backend response. Provide trackBy with a stable row identifier to avoid unnecessary row re‑rendering when the page changes.
Performance decisions for large data UIs
Server‑side pagination reduces memory and network transfer by fetching only the current page. CDK virtual scrolling reduces DOM size for client‑side large lists by rendering only visible rows. They solve different problems and are not interchangeable.
Use server pagination when filtering, sorting and paging must be done on the backend for correctness or cost. Use virtual scrolling when the full dataset fits in memory but DOM nodes do not. Combining both is possible but adds complexity.
State management shifts to you. Sorting, filtering and page resets must be coordinated manually to avoid stale data. A common bug is changing the sort without resetting pageIndex to zero, which can request a non‑existent page.
Accessibility and theming constraints
MatTable provides keyboard navigation and aria-rowindex out of the box. Custom cell templates must preserve semantic markup to retain screen reader support. Avoid replacing table cells with non‑semantic <div> elements.
MDC theming uses CSS custom properties. Deep selectors against internal DOM are fragile because the structure changed in v15. Prefer official theming tokens over deep overrides.
Limitations and how to check the result
Server‑side pagination moves complexity from rendering to state coordination. You must manage loading indicators, error states, and total length synchronization yourself.
Practical verification:
- Run
npm list @angular/materialin the project root to confirm version and standalone support. - Create a minimal table with MatPaginator and log page event payloads to confirm server request wiring.
- Inspect rendered table in browser dev tools for aria attributes and CSS custom property names to confirm MDC theming.
- Test keyboard navigation and focus behavior in custom cells to verify accessibility defaults are preserved.
Keep the DataSource thin, keep backend contracts explicit, and treat pagination, sorting and filtering as a single coordinated state machine rather than independent events.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.