Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2 cache observable http result data [duplicate]

I have a service that fetches data via the HTTP service and returns an observable object.

After the first call I would like to cache the result internally in the service, and once a new component will try to get the data it will take it from the cached result.

Is there a simple solution for this?

like image 241
Avi Avatar asked Jan 09 '17 18:01

Avi


3 Answers

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

like image 127
jonrsharpe Avatar answered Oct 05 '22 08:10

jonrsharpe


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.

like image 40
Stefdelec Avatar answered Oct 05 '22 07:10

Stefdelec


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

like image 38
yfranz Avatar answered Oct 05 '22 09:10

yfranz