TL;DR: You need to cache the observable returned from HttpClient
and combine it with shareReplay
and catchError
.
There’s a lot of situations when we need to ask the server the same thing again and again. Typical examples are asking for translations for the terms, or resolving some codes using the vocabulary located on a server.
We don’t want to bother the server too much, so we use caching. In this article, I will try to explain the proper way to cache HTTP calls using Angular and RxJS
So, suppose that we have a server that can return some nutrition information about a certain product. Let’s use the Open Food Facts API for that.
I give it the code of the produce (e.g. "7613034626844"
) and it gives me back an object with all the necessary information (e.g. {name: "Ovomaltine milk powder"}
). Let’s sketch an Angular service for that (see StackBlitz):
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient, HttpParams } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class ProductService {
private readonly URL = 'https://world.openfoodfacts.org/api/v0/product/';
constructor(private http: HttpClient) {
}
resolveProduct(code: string): Observable<any> {
console.log('Request is sent!')
return this.http.get(this.URL+code+'.json');
}
}
The service takes the code and passes it as a parameter to the API. We will create a super simple ProductComponent
and call it in the constructor.
Now, let’s use our ProductComponent
like so in the AppComponent
:
Sure enough, we will see that the request was performed three times, even though we were always asking the same information. Now, let’s make it faster using caching.
You may also like: Angular 8 RxJS Multiple HTTP Request using the forkJoin Example
The first idea for caching would be to actually cache the returned term, as I did here (see StackBlitz):
In theory, our service checks if the given term is presented in the cache, and, if so, returns it. If the product code is new, it asks the HTTP service to call it. When the server sends us the response, we put it into cache using the tap
operator.
For example, a user clicks a button to see the info about the code "7613034626844"
and our service will execute a call to a server.
When the users clicks the button next time to resolve the same term, our service will not initiate an HTTP call, using the cached value instead. At the first glance, everything works fine.
However, there is a caveat: we put the value into the cache asynchronously, and all asynchronous code will be executed after the synchronous code.
What if we call the resolve
function in a for
loop or in *ngFor
or just have several components using it within the same template? It turns out that our cache doesn’t do its job:
Suddenly, in the network tab, we see a lot of identical HTTP requests, so apparently, the cache can’t catch up.
The problem is that the reactions to HTTP calls happen only after all those HTTP calls are executed, because every asynchronous code is happening in the event loop.
We can fix this problem quite easily. Instead of caching the return values from the server, we can cache the observables that the Angular HttpClient
service returns.
This way, both function calling and caching will be synchronous. The only trick is that we must use the shareReplay
operator that will allow the subscribers to view the result of the HTTP call.
Without shareReplay
, the observable will be kept in the FINISHED
state after the request, and the new subscribers will not be able to get its value.
The result in the network tab looks exactly as we planned, we only have one HTTP call. Now, all that’s left is to manage the exceptions.
What will happen if our server was down for a couple of seconds while we were asking it to resolve a term for that? The error HTTP code will raise an error in the observable.
After the observable is errored or finished, there’s nothing we can do to restart it — that’s a contract for the observables.
With our current approach, it means that we’re caching the errors which is not a reasonable thing to do: the server will eventually start working and we would like to send it an HTTP request.
To achieve that, we can use the catchError
operator which would delete an observable from the cache, so that, next time, the server will be called.
You can use the [delete](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete)
operator for that, or assign false
to that key in the cache if you care about the performance more than readability.
The result: now we call the API only once.
This particular caching approach is called memoization and it will serve you well in the following cases:
All shortcomings of the memoization have a common reason, that is, we never clean our cached values. With that in mind, here are some restrictions:
Originally published by Yury Katkov at medium.com
#angular #web-development