Angular/RxJS 6: How to prevent duplicate HTTP requests?
After trying a few different methods, I came across this one that resolves my issue and only makes one HTTP request no matter how many subscribers there are:
class SharedService {
someDataObservable: Observable<any>;
constructor(private http: HttpClient) {}
getSomeData(): Observable<any> {
if (this.someDataObservable) {
return this.someDataObservable;
} else {
this.someDataObservable = this.http.get<any>('some/endpoint').pipe(share());
return this.someDataObservable;
}
}
}
I am still open to more efficient suggestions!
For the curious: share()
Based on your simplified scenario, I've built a working example but the interesting part is understanding what's going on.
First of all, I've built a service to mock HTTP and avoid making real HTTP calls:
export interface SomeData {
some: {
data: boolean;
};
}
@Injectable()
export class HttpClientMockService {
private cpt = 1;
constructor() {}
get<T>(url: string): Observable<T> {
return of({
some: {
data: true,
},
}).pipe(
tap(() => console.log(`Request n°${this.cpt++} - URL "${url}"`)),
// simulate a network delay
delay(500)
) as any;
}
}
Into AppModule
I've replaced the real HttpClient to use the mocked one:
{ provide: HttpClient, useClass: HttpClientMockService }
Now, the shared service:
@Injectable()
export class SharedService {
private cpt = 1;
public myDataRes$: Observable<SomeData> = this.http
.get<SomeData>("some-url")
.pipe(share());
constructor(private http: HttpClient) {}
getSomeData(): Observable<SomeData> {
console.log(`Calling the service for the ${this.cpt++} time`);
return this.myDataRes$;
}
}
If from the getSomeData
method you return a new instance, you'll have 2 different observables. Whether you use share or not. So the idea here is to "prepare" the request. CF myDataRes$
. It's just the request, followed by a share
. But it's only declared once and returning that reference from the getSomeData
method.
And now, if you subscribe from 2 different components to the observable (result of the service call), you'll have the following in your console:
Calling the service for the 1 time
Request n°1 - URL "some-url"
Calling the service for the 2 time
As you can see, we have 2 calls to the service, but only one request made.
Yeah!
And if you want to make sure that everything is working as expected, just comment out the line with .pipe(share())
:
Calling the service for the 1 time
Request n°1 - URL "some-url"
Calling the service for the 2 time
Request n°2 - URL "some-url"
But... It's far from ideal.
The delay
into the mocked service is cool to mock the network latency. But also hiding a potential bug.
From the stackblitz repro, go to component second
and uncomment the setTimeout. It'll call the service after 1s.
We notice that now, even if we're using share
from the service, we have the following:
Calling the service for the 1 time
Request n°1 - URL "some-url"
Calling the service for the 2 time
Request n°2 - URL "some-url"
Why that? Because when the first component subscribe to the observable, nothing happens for 500ms due to the delay (or the network latency). So the subscription is still alive during that time. Once the 500ms delay is done, the observable is completed (it's not a long lived observable, just like an HTTP request returns only one value, this one too because we're using of
).
But share
is nothing more than a publish
and refCount
. Publish allows us to multicast the result, and refCount allows us to close the subscription when nobody is listening to the observable.
So with your solution using share, if one of your component is created later than it takes to make the first request, you'll still have another request.
To avoid that, I cannot think any brilliant solution. Using multicast we'd have to then use the connect method, but where exactly? Making a condition and a counter to know whether it's the first call or not? Doesn't feel right.
So it's probably not the best idea and I'd be glad if someone can provide a better solution there, but in the meantime here's what we can do to keep the observable "alive":
private infiniteStream$: Observable<any> = new Subject<void>().asObservable();
public myDataRes$: Observable<SomeData> = merge(
this
.http
.get<SomeData>('some-url'),
this.infiniteStream$
).pipe(shareReplay(1))
As the infiniteStream$ is never closed, and we're merging both results plus using shareReplay(1)
, we now have the expect result:
One HTTP call even if multiple calls are made to the service. No matter how long the first request takes.
Here's a Stackblitz demo to illustrate all of that: https://stackblitz.com/edit/angular-n9tvx7