Angular  

Multi-Grid Synchronization with Shared Paging and Filters in Angular

Introduction

Many enterprise Angular applications display data using grids or tables. These grids often appear in pairs or sets, showing related information side by side. A very common real-world requirement is that multiple grids must remain synchronized. When the user changes the page in one grid, the other grid should update automatically. When the user applies a filter in one grid, all grids should reflect that filter.

These coordinated behaviours are known as multi-grid synchronization with shared paging and filters.

This pattern is widely required in domains such as banking dashboards, analytics platforms, inventory systems, HR systems, and admin consoles.

Understanding Multi-Grid Synchronization

What is Multi-Grid Synchronization?

In a dashboard-style layout, you may have:

  • Grid A showing summary or parent-level records

  • Grid B showing detailed records related to Grid A

  • Grid C showing metrics filtered by the selections in A or B

The expectation is that if a user changes any of these:

  • Page number

  • Page size

  • Sort order

  • Filters

  • Date range

  • Search term

Then all grids should behave consistently based on a shared state, rather than maintaining their own isolated states.

Why Shared Paging and Shared Filters?

Imagine two grids showing matching datasets:

  • Grid A: Customer list

  • Grid B: Customer orders

If you filter customers by country = India, Grid B should show only orders for Indian customers. If you move to page 4 in Grid A, Grid B should also load the corresponding page 4 of results.

This enhances:

  • User experience

  • Consistency

  • Predictability

  • Business accuracy

Typical enterprise use cases

  1. Master-detail dashboards: Parent grid controls child grid.

  2. Side-by-side comparisons: Two tables showing different calculations for the same filter conditions.

  3. Complex analytics: Multiple grids tracking a common filter set.

  4. Financial dashboards: Paging and date-range filters driving multiple analytics views.

Key Architectural Challenges

Building synchronized grids is not trivial.
The common challenges include:

Challenge 1: Avoiding Tight Coupling

If Grid A directly calls functions on Grid B, the system becomes difficult to maintain. A proper shared state management solution is required.

Challenge 2: Preventing Unnecessary API Calls

If each grid reloads data independently, you may create multiple calls to the same endpoint. Instead, you want controlled and debounced calls.

Challenge 3: Dealing with Race Conditions

When both grids request new data at the same time, results can get out of sync.

Challenge 4: Ensuring Reusability

Hardcoded filter logic inside components makes future changes costly.

Challenge 5: State Reset and Persistence

You need to support:

  • Reset to defaults

  • Remember last-used paging

  • Apply filters globally

High-Level Architecture for Multi-Grid Synchronization in Angular

A scalable approach includes:

  1. A shared state service that manages:

    • Paging

    • Filters

    • Sort conditions

    • Grid-level metadata

  2. A unified data-loading pipeline with RxJS behaviour subjects

  3. Each grid component subscribes to shared state

  4. API service listens to shared state updates and fetches data accordingly

  5. One-way data flow:
    Component → Shared State → Data Service → API → Results → Component

  6. Optional: Use NgRx or ComponentStore for larger applications

Designing the Shared Models

Paging Model

export interface Paging {
  page: number;
  pageSize: number;
}

Filter Model

export interface GridFilters {
  search?: string;
  country?: string;
  status?: string;
  dateRange?: {
    from: Date;
    to: Date;
  };
}

Sort Model

export interface SortState {
  active: string;
  direction: 'asc' | 'desc' | '';
}

Combined Grid State Model

export interface GridState {
  paging: Paging;
  filters: GridFilters;
  sort: SortState;
}

Implementing a Shared State Service in Angular

We use BehaviorSubject to store the latest state and allow components to subscribe.

grid-state.service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { GridState } from '../models/grid-state.model';

@Injectable({
  providedIn: 'root'
})
export class GridStateService {

  private initialState: GridState = {
    paging: { page: 1, pageSize: 10 },
    filters: {},
    sort: { active: '', direction: '' }
  };

  private stateSubject = new BehaviorSubject<GridState>(this.initialState);
  state$ = this.stateSubject.asObservable();

  updatePaging(page: number, pageSize: number) {
    const current = this.stateSubject.getValue();
    this.stateSubject.next({
      ...current,
      paging: { page, pageSize }
    });
  }

  updateFilters(filters: Partial<GridFilters>) {
    const current = this.stateSubject.getValue();
    this.stateSubject.next({
      ...current,
      filters: { ...current.filters, ...filters }
    });
  }

  updateSort(sort: SortState) {
    const current = this.stateSubject.getValue();
    this.stateSubject.next({
      ...current,
      sort
    });
  }

  reset() {
    this.stateSubject.next(this.initialState);
  }
}

This service acts as a single source of truth.

Unified Data Service for API Calls

grid-data.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, combineLatest } from 'rxjs';
import { GridStateService } from './grid-state.service';
import { switchMap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class GridDataService {

  constructor(
    private http: HttpClient,
    private gridState: GridStateService
  ) {}

  loadData(apiUrl: string): Observable<any> {
    return this.gridState.state$.pipe(
      switchMap(state => {
        let params = new HttpParams()
          .set('page', state.paging.page)
          .set('pageSize', state.paging.pageSize);

        Object.keys(state.filters).forEach(key => {
          params = params.set(key, String((state.filters as any)[key]));
        });

        if (state.sort.active) {
          params = params.set('sortBy', state.sort.active)
                         .set('sortDir', state.sort.direction);
        }

        return this.http.get(apiUrl, { params });
      })
    );
  }
}

Now both grids will receive synchronized data whenever the shared state changes.

Building Two Synchronized Grids

Grid Component Template

<table>
  <thead>
    <tr>
      <th (click)="sort('name')">Name</th>
      <th (click)="sort('country')">Country</th>
      <th (click)="sort('status')">Status</th>
    </tr>
  </thead>

  <tbody>
    <tr *ngFor="let row of data">
      <td>{{ row.name }}</td>
      <td>{{ row.country }}</td>
      <td>{{ row.status }}</td>
    </tr>
  </tbody>
</table>

<div class="pagination">
  <button (click)="prevPage()">Previous</button>
  <span>{{ paging.page }}</span>
  <button (click)="nextPage()">Next</button>
</div>

<div class="filters">
  <input type="text" placeholder="Search" (input)="applySearch($event.target.value)" />
</div>

Grid Component Class

import { Component, Input, OnInit } from '@angular/core';
import { GridDataService } from '../services/grid-data.service';
import { GridStateService } from '../services/grid-state.service';

@Component({
  selector: 'app-synced-grid',
  templateUrl: './synced-grid.component.html'
})
export class SyncedGridComponent implements OnInit {

  @Input() apiUrl: string;
  data: any[] = [];
  paging = { page: 1, pageSize: 10 };

  constructor(
    private dataService: GridDataService,
    private state: GridStateService
  ) {}

  ngOnInit() {
    this.dataService.loadData(this.apiUrl)
      .subscribe(result => {
        this.data = result.data;
        this.paging = result.paging;
      });
  }

  nextPage() {
    this.state.updatePaging(this.paging.page + 1, this.paging.pageSize);
  }

  prevPage() {
    if (this.paging.page > 1) {
      this.state.updatePaging(this.paging.page - 1, this.paging.pageSize);
    }
  }

  applySearch(value: string) {
    this.state.updateFilters({ search: value });
  }

  sort(column: string) {
    this.state.updateSort({
      active: column,
      direction: 'asc'
    });
  }
}

With this, multiple instances of app-synced-grid will automatically stay synchronized.

Example Usage in a Parent Component

<app-synced-grid apiUrl="/api/customers"></app-synced-grid>
<app-synced-grid apiUrl="/api/orders"></app-synced-grid>

Both grids will:

  • Use the same filters

  • Share the same paging

  • Respect the same sort order

But each grid will call its own API.

Best Practices for Multi-Grid Synchronization

Keep Shared State Independent

The shared state should contain only generic paging, filter, and sort information. Do not mix it with dataset-specific details.

Use Immutable Updates

Never mutate the state object directly.

Use Subjects wisely

BehaviorSubject is appropriate for always holding the latest value.

Debounce filter inputs

Add debounceTime(300) on search fields to avoid flooding the API.

Avoid over-fetching

Use distinctUntilChanged() for paging and filter streams.

Keep grids dumb, state smart

Grids should only display data and relay user events to the shared service.

Use trackBy in ngFor

Improves performance when dealing with large data sets.

Advanced: Using NgRx ComponentStore or Global Store

For very large applications, you can move shared state into:

  • NgRx Store

  • NgRx ComponentStore

  • Akita

  • RxState

ComponentStore is a good middle-ground, especially for a dashboard with multiple grids.

Performance Optimization Techniques

Memoize API results

Useful if grids show overlapping data.

Avoid re-rendering the entire grid

Use OnPush change detection.

@Component({
  selector: 'app-synced-grid',
  changeDetection: ChangeDetectionStrategy.OnPush,
  ...
})

Use virtual scrolling for large datasets

Angular CDK Virtual Scroll improves user experience.

Paginate on the server side

Do not load entire datasets to the browser.

Testing Multi-Grid Synchronization

Unit Testing GridStateService

Test:

  • Paging updates

  • Filter updates

  • Sort updates

  • Reset behavior

Use Jasmine or Jest for isolated testing.

Component Tests

Write tests to ensure:

  • Pagination buttons emit correct events

  • Input filters update shared state

  • Sort actions update shared state

Integration Tests

Use Cypress or Playwright to test:

  • Grid A and Grid B move together when paging

  • Filters from Grid A impact Grid B

  • Sort order propagates

These tests ensure that the entire synchronization chain works.

Conclusion

Multi-grid synchronization with shared paging and filters is a common and essential requirement in enterprise Angular applications. Implementing it correctly ensures a clean user experience, consistent business logic, and scalable architecture.

In this article, we covered:

  • Architectural challenges

  • Shared state design

  • Implementing BehaviorSubject-based state management

  • Building two synchronized grids

  • Best practices

  • Performance optimization

  • Testing strategies

By following this pattern, teams can create highly maintainable dashboards, reduce duplicated logic, and prevent inconsistencies across grids. The approach is simple enough for junior developers and scalable enough for senior developers building large applications.