Angular 2 cache observable http result data

I think you should not do a fetch() in the constructor or any time in the lifecycle of angular. And as you say, ngOnInit does not work in angular services.

Instead we want to leverage rxjs to seamlessly pass us cached values through the stream – without the caller having to know anything about cached vs non cached values.

If a component needs a data, it subscribes to it, regardless if it is cache or not. Why would you fetch() a data that you are not sure it will be used ?

Cache should be implement at a higher level. I think this kind of implementation is a good start : http://www.syntaxsuccess.com/viewarticle/caching-with-rxjs-observables-in-angular-2.0

getFriends(){
    if(!this._friends){
      this._friends = this._http.get('./components/rxjs-caching/friends.json')
                                   .map((res:Response) => res.json().friends)
                                   .publishReplay(1)
                                   .refCount();
    }
    return this._friends;
}

I am not sure it is the best way, but it is easier to maintain because it has a single responsability. Data would be cache only if a component subcribes to it, no matter what/who/which component needs the data and is the first to need it.


If you lean into observables as a means of sharing data, you can adopt the following approach:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Observable, ReplaySubject } from 'rxjs';

import { SomeModel } from 'path/to/it';

@Injectable({
  providedIn: 'root'
})
export class CachedService {
 
  private dataSubject = new ReplaySubject<SomeModel>(1);
  
  data$: Observable<SomeModel> = this.dataSubject.asObservable();

  constructor(private http: HttpClient) { }

  fetch() {
    this.http.get<SomeModel>(...).subscribe(res => this.dataSubject.next(res));
  }
}

This will make an HTTP call when the fetch method is called, and any subscribers to service.data$ will get the response from the ReplaySubject. As it replays earlier values, any subscribers who join after the HTTP call resolves will still get the previous response.

If you want to trigger an update, you can just call service.fetch() to kick off a new HTTP call and all subscribers will be updated once the new response arrives.

Your components would then look something like:

@Component({ ... })
export class SomeComponent implements OnDestroy, OnInit {

  private subscription?: Subscription;

  constructor(private service: CachedService) { }

  ngOnInit() {
    this.service.fetch();
    this.subscription = this.service.data$.subscribe(...);
  }

  ngOnDestroy() {
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
  }
}

I've recently written a blog article on this approach for my colleagues: http://blog.jonrshar.pe/2017/Apr/09/async-angular-data.html


You can build simple class Cacheable<> that helps managing cache of data retrieved from http server or other any other source:

declare type GetDataHandler<T> = () => Observable<T>;

export class Cacheable<T> {

    protected data: T;
    protected subjectData: Subject<T>;
    protected observableData: Observable<T>;
    public getHandler: GetDataHandler<T>;

    constructor() {
      this.subjectData = new ReplaySubject(1);
      this.observableData = this.subjectData.asObservable();
    }

    public getData(): Observable<T> {
      if (!this.getHandler) {
        throw new Error("getHandler is not defined");
      }
      if (!this.data) {
        this.getHandler().map((r: T) => {
          this.data = r;
          return r;
        }).subscribe(
          result => this.subjectData.next(result),
          err => this.subjectData.error(err)
        );
      }
      return this.observableData;
    }

    public resetCache(): void {
      this.data = null;
    }

    public refresh(): void {
      this.resetCache();
      this.getData();
    }

}

Usage

Declare Cacheable<> object (presumably as part of the service):

list: Cacheable<string> = new Cacheable<string>();

and handler:

this.list.getHandler = () => {
// get data from server
return this.http.get(url)
.map((r: Response) => r.json() as string[]);
}

Call from a component:

//gets data from server
List.getData().subscribe(…)

More details and code example are here: http://devinstance.net/articles/20171021/rxjs-cacheable