Avoiding duplicate HTTP requests in Angular when there are multiple subscribers

Avoiding duplicate HTTP requests in Angular when there are multiple subscribers

The problem

I wanted my angular app to load a setting from an API, but only wanted to load it once. The setting is likely to change so infrequently that it's not worth making repeated requests to the API.

The standard way to make a HTTP request in angular doesn't work well for this use case. Using the built-in HttpClient, typical code looks something like this:

getSettings(): Observable<SettingsDto> {
    return this.http.get<SettingsDto>('settings-url-here');
}

The HttpClient creates an observable, and the calling code subscribes to it to get the value from the API. However, each subscription triggers a fresh HTTP request.

My original solution

This is the code I wrote to modify the behaviour to what I wanted - a single HTTP request that is not repeated, even if the observable response from the getSettings method is subscribed to multiple times before the HTTP request finishes.

private settings$: ReplaySubject<SettingsDto> = undefined;
getSettings(): Observable<SettingsDto> {
    if(!this.settings$){
        this.settings$ = new ReplaySubject<SettingsDto>(1);
        this.http.get<SettingsDto>('settings-url-here')
            .pipe(tap(settings => this.settings$.next(settings)))
            .subscribe();
    }
    return this.settings$.asObservable().pipe(first());
}

The explanation

I declare a ReplaySubject. A Subject is a multicast observable (meaning it can have multiple subscribers), and a ReplaySubject will also broadcast past values to new subscribers. I leave it undefined to start with.

When the getSettings method is first called, the value of this.settings$ will be undefined, so the ReplaySubject will be created and the HTTP request will be made. (The ReplaySubject is created with a buffer size of 1, explained below.) When the response is received, I use the tap pipe to perform a side-effect. In this case, the side-effect is to emit the value of the settings to the ReplaySubject. I also subscribe to the Observable which the HttpClient gives me, as that is what triggers the actual HTTP request.

The Observable I return from getSettings is created from the ReplaySubject using the built-in asObservable method. I use the first pipe to ensure the Observable is completed when the value has been emitted, so there can't be a memory leak from it.

This means that every call to getSettings receives a new Observable, but they are all based on one single ReplaySubject. The ReplaySubject only ever receives one value, which is from the HTTP request, and that is the value which is provided to all subscribers. Because it is a ReplaySubject, new subscribers will also receive the value. I created it with a buffer size of 1, which is how many values will be broadcast to new subscribers when they subscribe. The value which will be broadcast is the most recent value, but in my use case that doesn't matter because there will only ever be a single value.

A better solution

While the above solution does work well, I later found a simpler way to achieve this. We can pipe the observable to shareReplay to achieve the same effect.

getSettings(): Observable<SettingsDto> {
    return this.http.get<SettingsDto>('settings-url-here')
        .pipe(shareReplay(1)); // only make one API request regardless of how many subscribers there are
}

Conclusion

With fairly minimal code, I have changed the behaviour of getSettings so that it only makes a single HTTP request, regardless of how many times the method is called.

Any thoughts or feedback? Let me know in the comments!