🧠 Lazy Loading HTTP Requests in Angular with RxJS and BehaviorSubject
When building modern Angular apps, it’s common to have services that fetch data from HTTP endpoints. But you don’t always want those calls to fire immediately, instead, you might want lazy loading: only fetch the data when it’s actually needed.
In this post, we’ll walk through how to lazily fetch and cache data in Angular using RxJS, and how to extend the pattern across multiple endpoints using a generic helper method.
🚀 The Problem
You want to:
- Trigger an HTTP request only when data is needed.
- Cache the result to avoid unnecessary network calls.
- Share the same data across multiple subscribers.
- Support more than one endpoint without duplicating logic.
✅ Solution: BehaviorSubject
+ RxJS + Generic Lazy Loader
We’ll use:
BehaviorSubject<T | null>
to hold the cached data.switchMap()
andtap()
to lazily fetch from HTTP.- A generic helper method to apply this pattern to multiple endpoints.
🔧 The Service (with Two Endpoints)
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class MyDataService {
private dataSubject = new BehaviorSubject<any | null>(null);
private data2Subject = new BehaviorSubject<any | null>(null);
constructor(private http: HttpClient) {}
// Lazy-loads and caches /api/my-data
getData(): Observable<any> {
return this.lazyLoad(this.dataSubject, '/api/my-data');
}
// Lazy-loads and caches /api/another-data
getData2(): Observable<any> {
return this.lazyLoad(this.data2Subject, '/api/another-data');
}
// Generic lazy loading and caching method
private lazyLoad<T>(subject: BehaviorSubject<T | null>, url: string): Observable<T> {
return subject.pipe(
switchMap((cached) => {
if (cached !== null) {
return of(cached); // Return cached value
} else {
return this.http.get<T>(url).pipe(
tap(response => subject.next(response)) // Cache the response
);
}
})
);
}
}
🧪 In a Component
@Component({
selector: 'app-my-component',
template: `
<div *ngIf="data$ | async as data; else loading">
</div>
<ng-template #loading>Loading...</ng-template>
`
})
export class MyComponent {
data$ = this.dataService.getData();
constructor(private dataService: MyDataService) {}
}
For a second component:
export class AnotherComponent {
data2$ = this.dataService.getData2();
}
🧠 Why This Pattern Works
Feature | How it Works |
---|---|
Lazy | No HTTP call until getData() is first subscribed |
Cached | BehaviorSubject holds the result for reuse |
Reactive | Multiple components can subscribe and receive the same data |
Reusable | lazyLoad() can be used for any number of endpoints |
🔁 Bonus: Add a Refresh Method
If you want to force a re-fetch:
refreshData(): void {
this.http.get('/api/my-data').subscribe(data => this.dataSubject.next(data));
}
Or, make it generic:
private refresh<T>(subject: BehaviorSubject<T | null>, url: string): void {
this.http.get<T>(url).subscribe(data => subject.next(data));
}
🧩 When to Use This Pattern
✅ Works great for:
- Caching static or rarely changing data
- Settings, metadata, or lookup tables
- Reusing data across multiple components
⚠️ Avoid for:
- Real-time or frequently updated data
- Parameterized calls where each param has different data
- Streaming APIs (use
Subject
orWebSocket
instead)