I wanted a simple, non-obtrusive Caching strategy for my Angular app. The aim is to provide a fast and a offline compatible experience to my app users in the most simplistic plug-and-play fashion. This is applicable to mobile apps and PWAs which want to provide some basic experience even in the absence of an internet connection.
How can we do this?
I create a HTTP Request interceptor which can access each and every request that is made by our app. This interceptor implements a caching strategy, a set of decisions about whether request can be cached, whether cache should be used in this request, when can we stop using the cache etc.
Let’s try to start with understanding the best user experience I want for my app.
- A user opens an app, network calls should be fast, especially if I had opened that page recently
- Network calls should not be duplicated or wasteful. This should happen for any requests happening within a threshold duration
- If user has no internet connection, every page should show the last data that was there in the app.
Caching Policy / Cache Strategy
Based on the above requirements, I try to define a Caching policy for the app.
- Our request interceptor can cache all
GET
andOPTIONS
requests which works very well assuming your server is idempotent. A URL along with the query parameters should be able to identify the request, and cache it. - Request caching is not forever, since things might change on the server.
- If offline, don’t try to make a request. Simply return result from Cache.
- If online, check if Cache duration has expired, else use Cache. Only in a case when the user is opening the same page after the Cache has expired, it makes sense to block the user on a network call.
This diagram would make things more clear.
Implementation
The cache strategy is really simple to reason with, and implement. Let’s dive into the code in detail. The example uses Ionic Native storage but you can tune it to instead use localstorage
as well with very minor differences.
// Class which we will use to store our data class CacheData { value: string; timestamp: number; }
We create a HTTP Interceptor which simply intercepts all my requests. This is the perfect place to Cache requests without affecting any of our existing code.
@Injectible export class CacheProvider implements HttpInterceptor {
This interceptor can be added in app.module.ts as
We depend only on the Ionic-storage plugin. If you want to use window.localstorage
, no dependencies needed.
constructor(public storage: Storage) {}
We need to define the intercept function where everything will happen. The intercept function can choose to process the request and return the response or pass on to the next handler to do whatever it wants.
intercept( request: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> {
OPTIONS and GET requests are Cached by us.
// Check if request is Cacheable. If not, skip. Since we are likely to have CORS requests, there will be a // OPTIONS request before our actual request. We can cache it too. if (request.method != 'GET' && request.method != 'OPTIONS') { return next.handle(request); }
Cache key takes in the request method and URL assuming that the server is stateless and will return the same data.
const cacheKey = request.method + "-" + request.urlWithParams;
We create an observable which contains the cache data from offline. The code is much simpler for localstorage
since the API is synchronous.
const offlineDataObservable: Observable<CacheData> = Observable.from( this.storage.get(cacheKey) );
Using flatMap we can build a final observable which can return either return offlineValue or return an observable with data from our API.
Here we implement our Cache strategy:
- First check if internet is not available, then return Offline data always.
- Check if Cache has not expired, return Offline data.
- Finally make the request and return the result from the API, and in process set the data in Cache.
return offlineDataObservable.flatMap(offlineData => { if (offlineData != null) { // No internet connection means that if (!navigator.onLine) { // Network is offline. Always use cached data return Observable.of(new HttpResponse(JSON.parse(offlineData.value))); } // Cache period not expired, return data from cache if (timeNow - offlineData.timestamp < CACHE_TIMEOUT) { console.log("Within timeout "); return Observable.of(new HttpResponse(JSON.parse(offlineData.value))); } } // We need to get data from server now and update our cache. // Although Ionic storage allows us to directly set and retrieve object from storage, if you modify the // HTTPResponse object somewhere, you will likely receive the modified object from storage. Hence, stringify // and save as soon as we get the object. return next.handle(request).do( event => { if (event instanceof HttpResponse) { console.log("API completed, storing in cache"); this.storage.set(cacheKey, <CacheData>{ value: JSON.stringify(event), timestamp: timeNow }); } }, err => {} ); });
Conclusion
What’s good a blog post without some parting words?
We have a simple plug and play solution which works very well in case of simple apps. If your app is going to get more dynamic, a possibility would be to have ability to have different Cache strategies for each request, for eg: some URLs should never be cached such as price information from an ecommerce site which keeps changing regularly.