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?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Avi
  • 261
  • 1
  • 3
  • 6

3 Answers3

70

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

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • 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). – Avi Jan 10 '17 at 12:39
  • 2
    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 – miniplus Mar 24 '17 at 08:58
  • Hi, how to implement a nice cache evict process with a scheduler ? calling fetch() service every hour ? – Clement Martino May 10 '17 at 08:53
  • @ClementMartino you mean clearing the cache? I'm not sure, I've never done that. – jonrsharpe May 10 '17 at 08:57
  • yes i mean clearing or updating the cache automatically – Clement Martino May 10 '17 at 08:59
  • do you have to unsubscribe on `ngOnDestroy` ? or is it taken care of automatically? – MonteCristo Jun 09 '17 at 11:54
  • @MonteCristo that's a good question, and I'm actually not certain of the answer. It's probably safest to keep a reference (`this.subscription = this.service.data$.subscribe(...)`) then as you say unsubscribe (`this.subscription.unsubscribe()`) when the component is destroyed. – jonrsharpe Jun 09 '17 at 12:17
  • I don't think it is a good implementation to fetch() in lifecycle. It should be trigger if request is needed. It is easier to maintain. – Stefdelec Aug 01 '17 at 06:16
  • @Stefdelec what it that *is* when the request is needed? You can fetch when and wherever you want or need to, if you have some other triggers that's fine, this was just to demonstrate the pattern. – jonrsharpe Aug 01 '17 at 06:19
  • What I mean by 'when needed', is when the component actually needs it, it subscribes and so it trigger the fetch. Cache should/could be implemented at a upper level. This kind of implementation is a better way : http://www.syntaxsuccess.com/viewarticle/caching-with-rxjs-observables-in-angular-2.0 – Stefdelec Aug 01 '17 at 06:21
  • 1
    @Stefdelec then write another answer showing that, I'm not saying this is the only way to do it. – jonrsharpe Aug 01 '17 at 06:21
  • How is it possible to refer to "dataSubject" when assigning the value for "data$", if the former is declared only after this line of code?? This is clearly an error in the code and shouldn't be done like that! Am I missing something here??... – TheCuBeMan May 10 '18 at 13:26
  • @TheCuBeMan hoisting – jonrsharpe May 10 '18 at 13:39
  • @jonrsharpe thanks for the answer!! I have implemented above pattern for update + fetch request . Like customer info needs to be updated and subsequently new fetch request is made to update info. However, as expected, all the observable subscribes to new request in all the component which is what i don't want. can i somehow unsubscribe for one particular component ? i used onDestroy hook which won't work. – candidJ Jun 27 '18 at 13:46
  • If you keep a handle on the subscription `this.subscription = this.service.data$.subscribe(...)` then you could later unsubscribe `this.subscription.unsubscribe()`. – jonrsharpe Jun 27 '18 at 14:02
  • 1
    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. – Chris Claude Jan 17 '22 at 12:55
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.

Graham
  • 7,431
  • 18
  • 59
  • 84
Stefdelec
  • 2,711
  • 3
  • 33
  • 40
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

izogfif
  • 6,000
  • 2
  • 35
  • 25
instanceMaster
  • 456
  • 3
  • 12