Search code examples
angularangular-httpclient

Angular 5 caching http service api calls


In my Angular 5 app a certain dataset (not changing very often) is needed multiple times on different places in the app. After the API is called, the result is stored with the Observable do operator. This way I implemented caching of HTTP requests within my service.

I'm using Angular 5.1.3 and RxJS 5.5.6.

Is this a good practise? Are there better alternatives?

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/do';

@Injectable()
export class FruitService {

  fruits: Array<string> = [];

  constructor(private http: HttpClient) { }

  getFruits() {
    if (this.fruits.length === 0) {
      return this.http.get<any>('api/getFruits')
        .do(data => { this.fruits = data })
    } else {
      return Observable.of(this.fruits);
    }
  }
}

Solution

  • 2022 edit

    You can create a cache operator for RxJS and configure it to use any kind of storage you want. Like below it will use, by default, the browser localStorage, but any thing that implements Storage will do, like sessionStorage or you can create your own memoryStorage that uses an inner Map<string, string>.

    const PENDING: Record<string, Observable<any>> = {};
    const CACHE_MISS: any = Symbol('cache-miss');
    
    export function cache<T>(
      key: string,
      storage: Storage = localStorage
    ): MonoTypeOperatorFunction<T> {
      return (source) =>
        defer(() => {
          const item = storage.getItem(key);
          if (typeof item !== 'string') {
            return of<T>(CACHE_MISS);
          }
          return of<T>(JSON.parse(item));
        }).pipe(
          switchMap((v) => {
            if (v === CACHE_MISS) {
              let pending = PENDING[key];
              if (!pending) {
                pending = source.pipe(
                  tap((v) => storage.setItem(key, JSON.stringify(v))),
                  finalize(() => delete PENDING[key]),
                  share({
                    connector: () => new ReplaySubject(1),
                    resetOnComplete: true,
                    resetOnError: true,
                    resetOnRefCountZero: true,
                  })
                );
                PENDING[key] = pending;
              }
              return pending;
            }
    
            return of(v);
          })
        );
    }
    

    Stackblitz example

    Old reply

    The problem with your solution is that if a 2nd call comes while a 1st one is pending, it create a new http request. Here is how I would do it:

    @Injectable()
    export class FruitService {
    
      readonly fruits = this.http.get<any>('api/getFruits').shareReplay(1);
    
      constructor(private http: HttpClient) { }
    }
    

    the bigger problem is when you have params and you want to cache based on the params. In that case you would need some sort of memoize function like the one from lodash (https://lodash.com/docs/4.17.5#memoize)

    You can also implement some in-memory cache operator for the Observable, like:

    const cache = {};
    
    function cacheOperator<T>(this: Observable<T>, key: string) {
        return new Observable<T>(observer => {
            const cached = cache[key];
            if (cached) {
                cached.subscribe(observer);
            } else {
                const add = this.multicast(new ReplaySubject(1));
                cache[key] = add;
                add.connect();
                add.catch(err => {
                    delete cache[key];
                    throw err;
                }).subscribe(observer);
            }
        });
    }
    
    declare module 'rxjs/Observable' {
        interface Observable<T> {
            cache: typeof cacheOperator;
        }
    }
    
    Observable.prototype.cache = cacheOperator;
    

    and use it like:

    getFruit(id: number) {
      return this.http.get<any>(`api/fruit/${id}`).cache(`fruit:${id}`);
    }