
On-Demand Data Loading in Master-Detail Grid Layouts
On-demand data loading is a powerful technique for scaling master-detail interfaces. Instead of fetching every detail upfront, you load related records only when the user needs them. Learn more in this article.
In our previous article, Getting Started with Master-Detail Layout Using Ignite UI for Angular Grid , we explored how to set up a clean and efficient master-detail interface. This pattern is ideal for presenting related records, such as orders with items or departments with employees, without cluttering the screen.
But what happens when your dataset is huge? Loading all the details for all records at once is inefficient. That’s where on-demand data loading comes in. Instead of fetching every possible detail upfront, you only load the data when the user expands a row, leading to faster initial renders, smoother interactions, and improved scalability.
In this article, we’ll dive into why on-demand loading matters, how to implement it in the Ignite UI for Angular Grid component, and best practices to make the most of it.
Extending the Master-Detail Template
If you’ve followed our first article, you already know how to set up a master grid with expandable rows and a detail template, and where to display data from:
- The same dataset – For example, orders and their associated items are bundled together
- An external dataset – for example, a service that takes customerID as a parameter and calls the API to retrieve customer orders data.
For large datasets, the second approach (external detail fetching) is where on-demand loading proves most effective.
Setting Up the Angular Project
Before we jump into the on-demand loading implementation, let’s prepare an Angular workspace for the demo. Here are the steps.
- Create a new Angular project
If you don’t already have one, start by generating a new Angular application using the Angular CLI and open the project in your preffered IDE.
ng new load-on-demand-grid cd load-on-demand-grid
- Install Ignite UI for Angular
We’ll be using the Ignite UI for Angular component library, which provides a rich set of UI components – most importantly the igxGrid, the foundation for our master-detail and on-demand loading examples. Add it to your project with:
ng add igniteui-angular
- Create a demo component
To keep everything organized, generate a dedicated Angular component where we’ll build the grid demo:
ng generate component pages/grid-demo
- Set up routing for the demo
Finally, adjust your routing configuration so the demo component is the default view. Replace your existing app.routes.ts (or your routing module) with a minimal setup.
import { Routes } from '@angular/router'; export const routes: Routes = [ { path: '', redirectTo: '/demo', pathMatch: 'full' }, { path: 'demo', loadComponent: () => import('./pages/grid-demo/grid-demo.component').then(m => m.GridDemoComponent) }, { path: '**', redirectTo: '/demo' } ];
Add provideHttpClient to the providers in app.config.ts.
export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimations(), provideHttpClient() ] };
Setting Up the Data
Before implementing on-demand loading in the master-detail grid, we need a reliable data layer. Preparing your data models and services ensures the grid displays meaningful content and can fetch related details as users interact with rows.
Your data for the IgxGrid can come from various sources:
- Static/local data – JSON files in the assets folder.
- Remote APIs – fetched via Angular’s HttpClient from a backend service.
- Mocked data – generated directly in a service during development.
For this example, we’ll connect to a custom remote Northwind Swagger API, which provides Customers and their related Orders. The concepts remain the same regardless of your data source structure.
Define Data Models
To enforce type safety and clarity, define TypeScript interfaces representing your data in a dedicated models.ts file. These models mirror the structure of the API responses and make it easy to bind data to the grid.
export interface AddressDto { street?: string; city?: string; region?: string; postalCode?: string; country?: string; phone?: string; } export interface CustomerDto { customerId?: string; companyName: string; contactName?: string; contactTitle?: string; address?: AddressDto; } export interface OrderDto { orderId: number; customerId: string; employeeId: number; shipperId?: number; orderDate?: Date; requiredDate?: Date; shipVia?: string; freight: number; shipName?: string; completed: boolean; shipAddress?: AddressDto; }
Create a Data Service
Next, centralize all API calls in a dedicated service – for example, northwind-swagger.service.ts. We’ll also add basic error handling to keep the app resilient.
const API_ENDPOINT = 'https://data-northwind.indigo.design'; @Injectable({ providedIn: 'root' }) export class NorthwindSwaggerService { constructor( private http: HttpClient ) { } public getCustomerDtoList(): Observable<CustomerDto[]> { return this.http.get<CustomerDto[]>(`${API_ENDPOINT}/Customers`) .pipe(catchError(this.handleError<CustomerDto[]>('getCustomerDtoList', []))); } public getOrderDtoList(id: string): Observable<OrderDto[]> { return this.http.get<OrderDto[]>(`${API_ENDPOINT}/Customers/${id}/Orders`) .pipe(catchError(this.handleError<OrderDto[]>('getOrderDtoList', []))); } private handleError<T>(operation = 'operation', result?: T) { return (error: any): Observable<T> => { console.error(`${operation} failed: ${error.message}`, error); return of(result as T); }; } }
Bind the Grid to Customer Data
With the models and service in place, the next step is to display the Customers dataset in the grid. The grid’s data property is bound to a northwindSwaggerCustomerDto array, which is populated when the component initializes by fetching data from the service.
<igx-grid [data]="northwindSwaggerCustomerDto" primaryKey="customerId" [allowFiltering]="true" filterMode="excelStyleFilter" class="grid"> <igx-column field="customerId" dataType="string" header="customerId" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> <igx-column field="companyName" dataType="string" header="companyName" [filterable]="true" [sortable]="true" required="true" [maxlength]="100" [selectable]="false"></igx-column> <igx-column field="contactName" dataType="string" header="contactName" [filterable]="true" [sortable]="true" [maxlength]="50" [selectable]="false"></igx-column> </igx-grid>
On the TypeScript side, add a service subscription in the GridDemoComponent class to retrieve the customers data and store it in the northwindSwaggerCustomerDto property. Then bind the grid data input property to `northwindSwaggerCustomerDto `.
@Component({ selector: 'app-grid-demo', standalone: true, imports: [IgxCheckboxComponent, IgxSimpleComboComponent, IgxGridComponent, IgxColumnComponent, IgxColumnMaxLengthValidatorDirective, NgIf, AsyncPipe, IgxGridDetailTemplateDirective], templateUrl: './grid-demo.component.html', styleUrl: './grid-demo.component.css' }) export class GridDemoComponent implements OnInit, OnDestroy{ private destroy$ = new Subject<void>(); public northwindSwaggerCustomerDto: CustomerDto[] = []; constructor(private northwindSwaggerService: NorthwindSwaggerService) {} ngOnInit() { this.northwindSwaggerService .getCustomerDtoList() .pipe(takeUntil(this.destroy$)) .subscribe(data => (this.northwindSwaggerCustomerDto = data)); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } }

Designing the Detail Template & Implementing Load-on-Demand
When the master grid presents many parent rows (customers), it’s inefficient to fetch every related child list (orders) for every row up front. Instead, load the details only when the user expands a row. This is the essence of on-demand loading: request child data only when it’s needed, then show it in the detail template.
How It Works?
- Put an igxGridDetail template inside the master igx-grid.
- In that template, call a method (e.g. getOrders(customerId)) that returns an Observable<OrderDto[]>.
- Use the async pipe to subscribe to that observable in the template so Angular renders results once available.
- Cache each customer’s observable/result so multiple expansions (or rapid change detection cycles) don’t trigger new HTTP calls.
How Do We Implement It?
- Add the detail template
First, we define a placeholder for the detail content by adding an ng-template with the igxGridDetail directive. This marks where the detail view for each expanded row will render:
<ng-template igxGridDetail let-rowData></ng-template>
- Add a combo bound to orders (loaded on demand)
Inside the template, we can render any component we want. In this example, we’ll add a combo box to display the orders for the selected customer. The key point here is that the orders are not loaded until the row is expanded.
<ng-container *ngIf="getOrders(rowData.customerId) | async as orders"> <div class="row-layout group"> <igx-simple-combo type="border" [data]="orders" displayKey="orderId" class="single-select-combo"> <label igxLabel>Order Id</label> </igx-simple-combo> </div> </ng-container>
The first time a row is expanded, the getOrders(customerId) method makes an HTTP call and returns an observable. Because of using the async pipe, Angular subscribes automatically and renders the combo once the data arrives.
A very important note here is that when we have a ng-template, Angular will keep loading data into this template every frame (change detection). If this data is loaded from a request, this means that for every visible template few requests will be made every second until the application inevitably freezes. This is why data needs to be cached after fetching.
private ordersCache = new Map<string, Observable<OrderDto[]>>(); getOrders(customerId: string): Observable<OrderDto[]> { if (!this.ordersCache.has(customerId)) { const request$ = this.northwindSwaggerService .getOrderDtoList(customerId) .pipe(take(1), shareReplay(1)); this.ordersCache.set(customerId, request$); } return this.ordersCache.get(customerId)!; }

A couple of things are happening here:
- We cache the observable in a Map, keyed by customerId. This ensures that once data has been fetched for a row, the same observable is reused instead of triggering new HTTP calls.
- take(1) guarantees the observable completes after the first response, which makes subscription management clean and avoids memory leaks.
- shareReplay(1) ensures that if multiple subscribers (or change detection cycles) request the same data, only one HTTP request is sent. It also replays the result to late subscribers (e.g., when the row is collapsed and expanded again).
- Then we store the data in ordersCache and return it
By combining caching with shareReplay, we make sure each customer row triggers at most one request, no matter how many times it is expanded or how often Angular runs change detection.
Displaying the Details for the Selected Order
So far, we’ve shown how to expand a customer row and fetch the list of orders on demand. Next, we want the user to select a specific order from that list and display its details right under the row.
- Add a selection event to the combo
We extend the combo box to notify us whenever a new order is selected. This is done by handling the selectionChanging event:
<igx-simple-combo type="border" [data]="orders" displayKey="orderId" (selectionChanging)="onOrderSelectionChange(rowData.customerId, $event.newValue)" class="single-select-combo" > <label igxLabel>Order Id</label> </igx-simple-combo>
- Track the selected order per customer
We use a Map<string, OrderDto> to keep track of the selection, keyed by the customerId. This ensures that each expanded row remembers its own selected order:
public selectedOrders = new Map<string, OrderDto>(); onOrderSelectionChange(customerId: string, order: OrderDto) { this.selectedOrders.set(customerId, order); } getSelectedOrder(customerId: string): OrderDto | undefined { return this.selectedOrders.get(customerId); }
- Show order details
Finally, once an order is selected, we can render its details in the expanded template. By calling getSelectedOrder(customerId), we retrieve the saved selection and display its fields:
<div *ngIf="getSelectedOrder(rowData.customerId) as selectedOrder" class="column-layout group_1"> <h6 class="h6">Order Details</h6> <div class="row-layout group_2"> <div class="column-layout group_3"> <div class="row-layout group_4"><p class="text">Completed</p></div> <div class="row-layout group_4"><p class="text">Shipper</p></div> <div class="row-layout group_4"><p class="text">Order Date</p></div> <div class="row-layout group_4"><p class="text">Country</p></div> <div class="row-layout group_4"><p class="text">City</p></div> <div class="row-layout group_4"><p class="text">Street</p></div> <div class="row-layout group_5"><p class="text">Postal Code</p></div> </div> <div class="column-layout group_6"> <div class="row-layout group_7"><igx-checkbox [checked]="!!selectedOrder?.completed" class="checkbox"></igx-checkbox></div> <div class="row-layout group_4"><p class="text">{{ selectedOrder?.shipperId }}</p></div> <div class="row-layout group_4"><p class="text">{{ selectedOrder?.orderDate }}</p></div> <div class="row-layout group_4"><p class="text">{{ selectedOrder?.shipAddress?.country }}</p></div> <div class="row-layout group_4"><p class="text">{{ selectedOrder?.shipAddress?.city }}</p></div> <div class="row-layout group_4"><p class="text">{{ selectedOrder?.shipAddress?.street }}</p></div> <div class="row-layout group_5"><p class="text_1">{{ selectedOrder?.shipAddress?.postalCode }}</p></div> </div> </div> </div> </div>
This gives us a simple yet functional detail view: when the user expands a customer row, they can pick an order from the combo, and the order details are displayed immediately underneath.

Cache Invalidation & Refresh Strategies
A cache that never expires can become stale or grow unbounded. You can choose to implement a strategy that fits your app:
Manual Refresh
Add a “Refresh” button in the detail template calling refreshOrders(customerId) that replaces the cache entry.
refreshOrders(customerId: string) { const request$ = this.northwindSwaggerService.getOrderDtoList(customerId).pipe(take(1), shareReplay(1)); this.ordersCache.set(customerId, request$); }
Clear on Collapse
Clear the cached observable when the row is collapsed. This ensures a fresh request next time the user expands:
<igx-grid (rowToggle)="onRowToggle($event)"...>
onRowToggle(event: IRowToggleEventArgs) { if (this.ordersCache.size > 0 && this.ordersCache.has(event.rowID)) { this.ordersCache.delete(event.rowID); } }
We’ve set up the master grid, implemented on-demand detail loading, cached results to avoid redundant requests, and even discussed refresh strategies.
Now let’s look at additional considerations and best practices that will help you scale this pattern to larger datasets and production scenarios.
Handling Large Detail Data
When the detail dataset itself is large, fetching and rendering everything at once can hurt performance. Instead, we use one of these strategies:
- Client or Server-side paging: Fetch only a slice of the data at a time by passing page and pageSize parameters to your orders API.
- Load-On-Demand: Fetching only the data that is needed at the moment
Memory & Lifecycle Considerations
- Clear caches in ngOnDestroy() to avoid memory leaks: this.ordersCache.clear().
- If you keep cached data (not just observables), consider limiting total memory.
- Avoid storing large, nested object Maps indefinitely.
Comparison with Preloaded Details
Approach | Best For | The Downsides |
---|---|---|
Preloaded details | Small datasets, simple demos, and quick prototypes | Slower initial load and higher memory usage |
On-demand loading | Large/complex datasets and production apps | Slightly more code complexity and requires async handling |
Conclusion
On-demand data loading is a natural extension of the Master-Detail Grid Layout. It improves performance, scales seamlessly with large datasets, and ensures a smooth user experience.
If you’re new to this pattern, check out our Getting Started with Master-Detail Layout Using Ignite UI for Angular Grid article first. Then, apply on-demand loading to your own apps to take your data grids to the next level.
With Ignite UI for Angular, you can build responsive, data-driven applications that stay fast, even as your data grows.
Check out the complete Load-on-demand app sample.