23

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?

2

3 Answers 3

72

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

Sign up to request clarification or add additional context in comments.

16 Comments

Hi, Thanks. I understand now. actually I uses the BehavoirSubjects and initialized it with null. However the onInite event is not working in services but that another issue (not really impotent).
ngOnInit is a lifecycleHook. If I'm not mistaken, services don't have lifecycleHooks. Instead of using ngOnInit, you should call this.fetch() in the constructor
Hi, how to implement a nice cache evict process with a scheduler ? calling fetch() service every hour ?
@Stefdelec then write another answer showing that, I'm not saying this is the only way to do it.
Assuming that my understanding of the logic is correct. If every component that uses the CachedService has to call the fetch method in ngOnInit(), then it defeats the purpose of caching.
|
12

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.

1 Comment

publishReplay is being deprecated
3

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

1 Comment

When does the cache actually get used though?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.