By using Http, we call a method that does a network call and returns an http observable:

getCustomer() {
    return this.http.get('/someUrl').map(res => res.json());
}

If we take this observable and add multiple subscribers to it:

let network$ = getCustomer();

let subscriber1 = network$.subscribe(...);
let subscriber2 = network$.subscribe(...);

What we want to do, is ensure that this does not cause multiple network requests.

This might seem like an unusual scenario, but its actually quite common: for example if the caller subscribes to the observable to display an error message, and passes it to the template using the async pipe, we already have two subscribers.

What is the correct way of doing that in RxJs 5?

Namely, this seems to work fine:

getCustomer() {
    return this.http.get('/someUrl').map(res => res.json()).share();
}

But is this the idiomatic way of doing this in RxJs 5, or should we do something else instead?

upvote
  flag
> share is identical to publish().refCount(). Actually it's not. See the following discussion: github.com/ReactiveX/rxjs/issues/1363 – Christian
upvote
  flag
edited question, according to the issue looks like the docs on the code need to be updated -> github.com/ReactiveX/rxjs/blob/master/src/operator/share.ts – Angular University
upvote
  flag
I think 'it depends'. But for calls where you can't cache the data locally b/c it might not make sense due to parameters changing/combinations .share() seems to absolutely be the right thing. But if you can cache things locally some of the other answers regarding ReplaySubject/BehaviorSubject are also good solutions. – JimB
upvote
  flag
I think not only we need cache the data, we also need update/modify the data cached. It's a common case. For example, if I want to add a new field to the model cached or update the value of field. Maybe create a singleton DataCacheService with CRUD method is a better way? Like store of Redux. What do you think? – novaline

18 Answers 11

Have you tried running the code you already have?

Because you are constructing the Observable from the promise resulting from getJSON(), the network request is made before anyone subscribes. And the resulting promise is shared by all subscribers.

var promise = jQuery.getJSON(requestUrl); // network call is executed now
var o = Rx.Observable.fromPromise(promise); // just wraps it in an observable
o.subscribe(...); // does not trigger network call
o.subscribe(...); // does not trigger network call
// ...
upvote
  flag
i've edit the question to make it Angular 2 specific – Angular University

Cache the data and if available cached, return this otherwise make the HTTP request.

import {Injectable} from '@angular/core';
import {Http, Headers} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/of'; //proper way to import the 'of' operator
import 'rxjs/add/operator/share';
import 'rxjs/add/operator/map';
import {Data} from './data';

@Injectable()
export class DataService {
  private url:string = 'https://cors-test.appspot.com/test';

  private data: Data;
  private observable: Observable<any>;

  constructor(private http:Http) {}

  getData() {
    if(this.data) {
      // if `data` is available just return it as `Observable`
      return Observable.of(this.data); 
    } else if(this.observable) {
      // if `this.observable` is set then the request is in progress
      // return the `Observable` for the ongoing request
      return this.observable;
    } else {
      // example header (not necessary)
      let headers = new Headers();
      headers.append('Content-Type', 'application/json');
      // create the request, store the `Observable` for subsequent subscribers
      this.observable = this.http.get(this.url, {
        headers: headers
      })
      .map(response =>  {
        // when the cached data is available we don't need the `Observable` reference anymore
        this.observable = null;

        if(response.status == 400) {
          return "FAILURE";
        } else if(response.status == 200) {
          this.data = new Data(response.json());
          return this.data;
        }
        // make it shared so more than one subscriber can get the result
      })
      .share();
      return this.observable;
    }
  }
}

Plunker example

upvote
  flag
thanks for the answer! could you explain that's the purpose of do? is it the same if i put the logic inside do block into a function passed to map? i.e. .map(this.extractData)? – totoro
1 upvote
  flag
do() in contrary to map() doesn't modify the event. You could use map() as well but then you had to ensure the correct value is returned at the end of the callback. – Günter Zöchbauer
upvote
  flag
you mean do() does not modify the event? how does map() modify it? is it okay if i set local variables inside .map(this.extractData) without returning anything? thanks! – totoro
1 upvote
  flag
If the call-site that does the .subscribe() doesn't need the value you can do that because it might get just null (depending on what this.extractData returns), but IMHO this doesn't express the intent of the code well. – Günter Zöchbauer
upvote
  flag
thanks for the reply! in your code, is there any chance of returning null? say due to a certain order of execution (inside the final else block) where this.observable=null is executed before return this.observable? – totoro
upvote
  flag
When this.extraData ends like extraData() { if(foo) { doSomething();}} otherwise the result of the last expression is returned which might not be what you want. – Günter Zöchbauer
1 upvote
  flag
I finally got this working; couldn't get this to work properly with a component that calls a service with a function that calls http; I had five async pipes in my component's template, and the XHR request was being made five times even with .share() after the http.get(): until I tried adding .share() in my search component, as in: .debounceTime(300).distinctUntilChanged().switchMap(...[stuf‌​f]...).share(); the async pipe was subscribing multiple times, with share() after, it only fired the call to the service function (and thus http) once. Yay! – Harry
upvote
  flag
If I have multiple components requesting the same data and I know that it will not change, does it make sense to use the approach, or is there an alternative way of doing so? I've managed to get this example working and it's working fine, my only question is whether my use-case requires this setup. – Daniel Grima
upvote
  flag
The tricky part is to return an observable that emits the correct data for every possible case (before the first request to the server, during the first request of the server, after the response from the server is available. There is not much difference if you need only a single call or multiple. Multiple calls just lead to the 1st and 2nd case happening again after the 3rd case. I don't know observable operators well enough to be able to tell if there are more concise ways that cover this as well. – Günter Zöchbauer
upvote
  flag
Yep exactly, so far I followed this example and managed to have this in a base service class so that all services can optionally have shared data. After I've seen it work I'm just wondering whether I need to set shared() to the observable when I know that there won't be simultaneous calls to the same observable. I'm guessing that I could use the same implementation with a few minor changes as I'm trying to have data returned from a service shared. – Daniel Grima
upvote
  flag
Is this method still compatible with latest angular version 2.1.0 after the release of the stable version? – Saiyaff Farouk
upvote
  flag
There is no Angular involved at all (except the @Injectable() and that the Observable is returned from Http). That's pure rxjs. And yes, it's compatible ;-) – Günter Zöchbauer
1 upvote
  flag
IMO that's close to the best solution. If you need to serve different objects use a map to act as a cache. And if you need to push updates (e.g. polling an API), use replay subjects and let your components subscribe to them. – curiosity
4 upvote
  flag
@Günter, thank you for the code, it works. However, I am trying to understand why you are keeping track of Data and Observable separately. Wouldn't you effectively achieve the same effect by caching just Observable<Data> like this? if (this.observable) { return this.observable; } else { this.observable = this.http.get(url) .map(res => res.json().data); return this.observable; } – July.Tech
upvote
  flag
import {Data} from './data'; What is data and why this was used? – Harleen Kaur Arora
1 upvote
  flag
@HarleenKaur It's a class the received JSON is deserialized to, to get strong type checking and autocompletion. There is no need to use it, but it's common. – Günter Zöchbauer
upvote
  flag
HI @GünterZöchbauer - can you elaborate how to parametize the "getData()" method? E.g. getCustomerWithId(id:string) and make it an observable as well? I'm quite confused by the "data" object - it seems to me this implementation can only "cache" one element at a time? I'm quite new to RxJS and i'm probably missing some obvious part here...or what? – Hoof
upvote
  flag
You can make data an array or object with a property per value and pass the index or property name to data and then always access data[idx] or data[propName]. You also need to change the observable the same way and have one observable per kind of data. You can also duplicate getData() and have getImages(), getUsers(), ... Also the .publishLast().refCount() seems to be quite popular. I haven't tried that. I guess mine is more helpful to understand what happens kinda step-by-step. – Günter Zöchbauer
upvote
  flag
Ok -thanks. I'll give it a go :) – Hoof

I starred the question, but i'll try and have a go at this.

//this will be the shared observable that 
//anyone can subscribe to, get the value, 
//but not cause an api request
let customer$ = new Rx.ReplaySubject(1);

getCustomer().subscribe(customer$);

//here's the first subscriber
customer$.subscribe(val => console.log('subscriber 1: ' + val));

//here's the second subscriber
setTimeout(() => {
  customer$.subscribe(val => console.log('subscriber 2: ' + val));  
}, 1000);

function getCustomer() {
  return new Rx.Observable(observer => {
    console.log('api request');
    setTimeout(() => {
      console.log('api response');
      observer.next('customer object');
      observer.complete();
    }, 500);
  });
}

Here's the proof :)

There is but one takeaway: getCustomer().subscribe(customer$)

We are not subscribing to the api response of getCustomer(), we are subscribing to a ReplaySubject which is observable which is also able to subscribe to a different Observable and (and this is important) hold it's last emitted value and republish it to any of it's(ReplaySubject's) subscribers.

upvote
  flag
I like this approach as it makes good use of rxjs and no need to add custom logic, thank-you – Thibs

Per @Cristian suggestion, this is one way that works well for HTTP observables, that only emit once and then they complete:

getCustomer() {
    return this.http.get('/someUrl')
        .map(res => res.json()).publishLast().refCount();
}
upvote
  flag
There are a couple of problems with using this approach - the returned observable cannot be cancelled or retried. This might not be an issue for you, but then again it might. If this is a problem then the share operator might be a reasonable choice (albeit with some nasty edge cases). For a deep dive discussion on the options see comments section in this blog post: blog.jhades.org/… – Christian
upvote
  flag
Small clarification... Although strictly the source observable being shared by publishLast().refCount() cannot be cancelled, once all subscriptions to the observable returned by refCount have been cancelled, the net effect is the source observable will be unsubscribed, cancelling it if it where "inflight" – Christian
upvote
  flag
@Christian Hey, can you explain what you mean by saying "cannot be cancelled or retried"? Thanks. – undefined

Just call share() after map and before any subscribe.

In my case, I have a generic service (RestClientService.ts) who is making the rest call, extracting data, check for errors and returning observable to a concrete implementation service (f.ex.: ContractClientService.ts), finally this concrete implementation returns observable to de ContractComponent.ts, and this one subscribe to update the view.

RestClientService.ts:

export abstract class RestClientService<T extends BaseModel> {

      public GetAll = (path: string, property: string): Observable<T[]> => {
        let fullPath = this.actionUrl + path;
        let observable = this._http.get(fullPath).map(res => this.extractData(res, property));
        observable = observable.share();  //allows multiple subscribers without making again the http request
        observable.subscribe(
          (res) => {},
          error => this.handleError2(error, "GetAll", fullPath),
          () => {}
        );
        return observable;
      }

  private extractData(res: Response, property: string) {
    ...
  }
  private handleError2(error: any, method: string, path: string) {
    ...
  }

}

ContractService.ts:

export class ContractService extends RestClientService<Contract> {
  private GET_ALL_ITEMS_REST_URI_PATH = "search";
  private GET_ALL_ITEMS_PROPERTY_PATH = "contract";
  public getAllItems(): Observable<Contract[]> {
    return this.GetAll(this.GET_ALL_ITEMS_REST_URI_PATH, this.GET_ALL_ITEMS_PROPERTY_PATH);
  }

}

ContractComponent.ts:

export class ContractComponent implements OnInit {

  getAllItems() {
    this.rcService.getAllItems().subscribe((data) => {
      this.items = data;
   });
  }

}

according to this article

It turns out we can easily add caching to the observable by adding publishReplay(1) and refCount.

so inside if statements just append

.publishReplay(1)
.refCount();

to .map(...)

I found a way to store the http get result into sessionStorage and use it for the session, so that it will never call the server again.

I used it to call github API to avoid usage limit.

@Injectable()
export class HttpCache {
  constructor(private http: Http) {}

  get(url: string): Observable<any> {
    let cached: any;
    if (cached === sessionStorage.getItem(url)) {
      return Observable.of(JSON.parse(cached));
    } else {
      return this.http.get(url)
        .map(resp => {
          sessionStorage.setItem(url, resp.text());
          return resp.json();
        });
    }
  }
}

FYI, sessionStorage limit is 5M(or 4.75M). So, it should not be used like this for large set of data.

upvote
  flag
If you will store in session Storage then How will you make sure that Session storage is destroyed when you leave the app ? – Gags

I wrote a cache class,

/**
 * Caches results returned from given fetcher callback for given key,
 * up to maxItems results, deletes the oldest results when full (FIFO).
 */
export class StaticCache
{
    static cachedData: Map<string, any> = new Map<string, any>();
    static maxItems: number = 400;

    static get(key: string){
        return this.cachedData.get(key);
    }

    static getOrFetch(key: string, fetcher: (string) => any): any {
        let value = this.cachedData.get(key);

        if (value != null){
            console.log("Cache HIT! (fetcher)");
            return value;
        }

        console.log("Cache MISS... (fetcher)");
        value = fetcher(key);
        this.add(key, value);
        return value;
    }

    static add(key, value){
        this.cachedData.set(key, value);
        this.deleteOverflowing();
    }

    static deleteOverflowing(): void {
        if (this.cachedData.size > this.maxItems) {
            this.deleteOldest(this.cachedData.size - this.maxItems);
        }
    }

    /// A Map object iterates its elements in insertion order — a for...of loop returns an array of [key, value] for each iteration.
    /// However that seems not to work. Trying with forEach.
    static deleteOldest(howMany: number): void {
        //console.debug("Deleting oldest " + howMany + " of " + this.cachedData.size);
        let iterKeys = this.cachedData.keys();
        let item: IteratorResult<string>;
        while (howMany-- > 0 && (item = iterKeys.next(), !item.done)){
            //console.debug("    Deleting: " + item.value);
            this.cachedData.delete(item.value); // Deleting while iterating should be ok in JS.
        }
    }

    static clear(): void {
        this.cachedData = new Map<string, any>();
    }

}

It's all static because of how we use it, but feel free to make it a normal class and a service. I'm not sure if angular keeps a single instance for the whole time though (new to Angular2).

And this is how I use it:

            let httpService: Http = this.http;
            function fetcher(url: string): Observable<any> {
                console.log("    Fetching URL: " + url);
                return httpService.get(url).map((response: Response) => {
                    if (!response) return null;
                    if (typeof response.json() !== "array")
                        throw new Error("Graph REST should return an array of vertices.");
                    let items: any[] = graphService.fromJSONarray(response.json(), httpService);
                    return array ? items : items[0];
                });
            }

            // If data is a link, return a result of a service call.
            if (this.data[verticesLabel][name]["link"] || this.data[verticesLabel][name]["_type"] == "link")
            {
                // Make an HTTP call.
                let url = this.data[verticesLabel][name]["link"];
                let cachedObservable: Observable<any> = StaticCache.getOrFetch(url, fetcher);
                if (!cachedObservable)
                    throw new Error("Failed loading link: " + url);
                return cachedObservable;
            }

I assume there could be a more clever way, which would use some Observable tricks but this was just fine for my purposes.

Just use this cache layer, it does everything you requires, and even manage cache for ajax requests.

http://www.ravinderpayal.com/blogs/12Jan2017-Ajax-Cache-Mangement-Angular2-Service.html

It's this much easy to use

@Component({
    selector: 'home',
    templateUrl: './html/home.component.html',
    styleUrls: ['./css/home.component.css'],
})
export class HomeComponent {
    constructor(AjaxService:AjaxService){
        AjaxService.postCache("/api/home/articles").subscribe(values=>{console.log(values);this.articles=values;});
    }

    articles={1:[{data:[{title:"first",sort_text:"description"},{title:"second",sort_text:"description"}],type:"Open Source Works"}]};
}

The layer(as an inject-able angular service) is

import { Injectable }     from '@angular/core';
import { Http, Response} from '@angular/http';
import { Observable }     from 'rxjs/Observable';
import './../rxjs/operator'
@Injectable()
export class AjaxService {
    public data:Object={};
    /*
    private dataObservable:Observable<boolean>;
     */
    private dataObserver:Array<any>=[];
    private loading:Object={};
    private links:Object={};
    counter:number=-1;
    constructor (private http: Http) {
    }
    private loadPostCache(link:string){
     if(!this.loading[link]){
               this.loading[link]=true;
               this.links[link].forEach(a=>this.dataObserver[a].next(false));
               this.http.get(link)
                   .map(this.setValue)
                   .catch(this.handleError).subscribe(
                   values => {
                       this.data[link] = values;
                       delete this.loading[link];
                       this.links[link].forEach(a=>this.dataObserver[a].next(false));
                   },
                   error => {
                       delete this.loading[link];
                   }
               );
           }
    }

    private setValue(res: Response) {
        return res.json() || { };
    }

    private handleError (error: Response | any) {
        // In a real world app, we might use a remote logging infrastructure
        let errMsg: string;
        if (error instanceof Response) {
            const body = error.json() || '';
            const err = body.error || JSON.stringify(body);
            errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
        } else {
            errMsg = error.message ? error.message : error.toString();
        }
        console.error(errMsg);
        return Observable.throw(errMsg);
    }

    postCache(link:string): Observable<Object>{

         return Observable.create(observer=> {
             if(this.data.hasOwnProperty(link)){
                 observer.next(this.data[link]);
             }
             else{
                 let _observable=Observable.create(_observer=>{
                     this.counter=this.counter+1;
                     this.dataObserver[this.counter]=_observer;
                     this.links.hasOwnProperty(link)?this.links[link].push(this.counter):(this.links[link]=[this.counter]);
                     _observer.next(false);
                 });
                 this.loadPostCache(link);
                 _observable.subscribe(status=>{
                     if(status){
                         observer.next(this.data[link]);
                     }
                     }
                 );
             }
            });
        }
}
1 upvote
  flag
what's a point of down-voting an answer without stating a reason? – Ravinder Payal

UPDATE: Ben Lesh says the next minor release after 5.2.0, you'll be able to just call shareReplay() to truly cache.

PREVIOUSLY.....

Firstly, don't use share() or publishReplay(1).refCount(), they are the same and the problem with it, is that it only shares if connections are made while the observable is active, if you connect after it completes, it creates a new observable again, translation, not really caching.

Birowski gave the right solution above, which is to use ReplaySubject. ReplaySubject will caches the values you give it (bufferSize) in our case 1. It will not create a new observable like share() once refCount reaches zero and you make a new connection, which is the right behavior for caching.

Here's a reusable function

export function cacheable<T>(o: Observable<T>): Observable<T> {
  let replay = new ReplaySubject<T>(1);
  o.subscribe(
    x => replay.next(x),
    x => replay.error(x),
    () => replay.complete()
  );
  return replay.asObservable();
}

Here's how to use it

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { cacheable } from '../utils/rxjs-functions';

@Injectable()
export class SettingsService {
  _cache: Observable<any>;
  constructor(private _http: Http, ) { }

  refresh = () => {
    if (this._cache) {
      return this._cache;
    }
    return this._cache = cacheable<any>(this._http.get('YOUR URL'));
  }
}

Below is a more advance version of the cacheable function This one allows has its own lookup table + the ability to provide a custom lookup table. This way, you don't have to check this._cache like in the above example. Also notice that instead of passing the observable as the first argument, you pass a function which returns the observables, this is because Angular's Http executes right away, so by returning a lazy executed function, we can decide not to call it if it's already in our cache.

let cacheableCache: { [key: string]: Observable<any> } = {};
export function cacheable<T>(returnObservable: () => Observable<T>, key?: string, customCache?: { [key: string]: Observable<T> }): Observable<T> {
  if (!!key && (customCache || cacheableCache)[key]) {
    return (customCache || cacheableCache)[key] as Observable<T>;
  }
  let replay = new ReplaySubject<T>(1);
  returnObservable().subscribe(
    x => replay.next(x),
    x => replay.error(x),
    () => replay.complete()
  );
  let observable = replay.asObservable();
  if (!!key) {
    if (!!customCache) {
      customCache[key] = observable;
    } else {
      cacheableCache[key] = observable;
    }
  }
  return observable;
}

Usage:

getData() => cacheable(this._http.get("YOUR URL"), "this is key for my cache")

rxjs 5.3.0

I haven't been happy with .map(myFunction).publishReplay(1).refCount()

With multiple subscribers, .map() executes myFunction twice in some cases (I expect it to only execute once). One fix seems to be publishReplay(1).refCount().take(1)

Another thing you can do, is just not use refCount() and make the Observable hot right away:

let obs = this.http.get('my/data.json').publishReplay(1);
obs.connect();
return obs;

This will start the HTTP request regardless of subscribers. I'm not sure if unsubscribing before the HTTP GET finishes will cancel it or not.

I assume that @ngx-cache/core could be useful to maintain caching features for the http calls, especially if the HTTP call is made both on browser and server platforms.

Let's say we have the following method:

getCustomer() {
  return this.http.get('/someUrl').map(res => res.json());
}

You can use the Cached decorator of @ngx-cache/core to store the returned value from the method making the HTTP call at the cache storage (the storage can be configurable, please check the implementation at ng-seed/universal) - right on the first execution. The next times the method is invoked (no matter on browser or server platform), the value is retrieved from the cache storage.

import { Cached } from '@ngx-cache/core';

...

@Cached('get-customer') // the cache key/identifier
getCustomer() {
  return this.http.get('/someUrl').map(res => res.json());
}

There's also the possibility to use caching methods (has, get, set) using the caching API.

anyclass.ts

...
import { CacheService } from '@ngx-cache/core';

@Injectable()
export class AnyClass {
  constructor(private readonly cache: CacheService) {
    // note that CacheService is injected into a private property of AnyClass
  }

  // will retrieve 'some string value'
  getSomeStringValue(): string {
    if (this.cache.has('some-string'))
      return this.cache.get('some-string');

    this.cache.set('some-string', 'some string value');
    return 'some string value';
  }
}

Here are the list of packages, both for client-side and server-side caching:

The implementation you choose is going to depend on if you want unsubscribe() to cancel your HTTP request or not.

In any case, TypeScript decorators are a nice way of standardizing behavior. This is the one I wrote:

  @CacheObservableArgsKey
  getMyThing(id: string): Observable<any> {
    return this.http.get('things/'+id);
  }

Decorator definition:

/**
 * Decorator that replays and connects to the Observable returned from the function.
 * Caches the result using all arguments to form a key.
 * @param target
 * @param name
 * @param descriptor
 * @returns {PropertyDescriptor}
 */
export function CacheObservableArgsKey(target: Object, name: string, descriptor: PropertyDescriptor) {
  const originalFunc = descriptor.value;
  const cacheMap = new Map<string, any>();
  descriptor.value = function(this: any, ...args: any[]): any {
    const key = args.join('::');

    let returnValue = cacheMap.get(key);
    if (returnValue !== undefined) {
      console.log(`${name} cache-hit ${key}`, returnValue);
      return returnValue;
    }

    returnValue = originalFunc.apply(this, args);
    console.log(`${name} cache-miss ${key} new`, returnValue);
    if (returnValue instanceof Observable) {
      returnValue = returnValue.publishReplay(1);
      returnValue.connect();
    }
    else {
      console.warn('CacheHttpArgsKey: value not an Observable cannot publishReplay and connect', returnValue);
    }
    cacheMap.set(key, returnValue);
    return returnValue;
  };

  return descriptor;
}
upvote
  flag
Hi @Arlo - the example above does not compile. Property 'connect' does not exist on type '{}'. from the line returnValue.connect();. Can you elaborate? – Hoof

rxjs 5.4.0 has a new shareReplay method.

The author explicitly says "ideal for handling things like caching AJAX results"

rxjs PR #2443 feat(shareReplay): adds shareReplay variant of publishReplay

shareReplay returns an observable that is the source multicasted over a ReplaySubject. That replay subject is recycled on error from the source, but not on completion of the source. This makes shareReplay ideal for handling things like caching AJAX results, as it's retryable. It's repeat behavior, however, differs from share in that it will not repeat the source observable, rather it will repeat the source observable's values.

upvote
  flag
Is it related to this? These docs are from 2014 though. github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core‌​/… – Aaron Hoffman
4 upvote
  flag
I tried adding .shareReplay(1, 10000) to an observable but I didn't notice any caching or behavior change. Is there a working example available? – Aydus-Matthew

Cacheable HTTP Response Data using Rxjs Observer/Observable + Caching + Subscription

See Code Below

*disclaimer: I am new to rxjs, so bear in mind that I may be misusing the observable/observer approach. My solution is purely a conglomeration of other solutions I found, and is the consequence of having failed to find a simple well-documented solution. Thus I am providing my complete code solution (as I would liked to have found) in hopes that it helps others.

*note, this approach is loosely based on GoogleFirebaseObservables. Unfortunately I lack the proper experience/time to replicate what they did under the hood. But the following is a simplistic way of providing asynchronous access to some cache-able data.

Situation: A 'product-list' component is tasked with displaying a list of products. The site is a single-page web app with some menu buttons that will 'filter' the products displayed on the page.

Solution: The component "subscribes" to a service method. The service method returns an array of product objects, which the component accesses through the subscription callback. The service method wraps its activity in a newly created Observer and returns the observer. Inside this observer, it searches for cached data and passes it back to the subscriber (the component) and returns. Otherwise it issues an http call to retrieve the data, subscribes to the response, where you can process that data (e.g. map the data to your own model) and then pass the data back to the subscriber.

The Code

product-list.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { ProductService } from '../../../services/product.service';
import { Product, ProductResponse } from '../../../models/Product';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.scss']
})
export class ProductListComponent implements OnInit {
  products: Product[];

  constructor(
    private productService: ProductService
  ) { }

  ngOnInit() {
    console.log('product-list init...');
    this.productService.getProducts().subscribe(products => {
      console.log('product-list received updated products');
      this.products = products;
    });
  }
}

product.service.ts

import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { Observable, Observer } from 'rxjs';
import 'rxjs/add/operator/map';
import { Product, ProductResponse } from '../models/Product';

@Injectable()
export class ProductService {
  products: Product[];

  constructor(
    private http:Http
  ) {
    console.log('product service init.  calling http to get products...');

  }

  getProducts():Observable<Product[]>{
    //wrap getProducts around an Observable to make it async.
    let productsObservable$ = Observable.create((observer: Observer<Product[]>) => {
      //return products if it was previously fetched
      if(this.products){
        console.log('## returning existing products');
        observer.next(this.products);
        return observer.complete();

      }
      //Fetch products from REST API
      console.log('** products do not yet exist; fetching from rest api...');
      let headers = new Headers();
      this.http.get('http://localhost:3000/products/',  {headers: headers})
      .map(res => res.json()).subscribe((response:ProductResponse) => {
        console.log('productResponse: ', response);
        let productlist = Product.fromJsonList(response.products); //convert service observable to product[]
        this.products = productlist;
        observer.next(productlist);
      });
    }); 
    return productsObservable$;
  }
}

product.ts (the model)

export interface ProductResponse {
  success: boolean;
  msg: string;
  products: Product[];
}

export class Product {
  product_id: number;
  sku: string;
  product_title: string;
  ..etc...

  constructor(product_id: number,
    sku: string,
    product_title: string,
    ...etc...
  ){
    //typescript will not autoassign the formal parameters to related properties for exported classes.
    this.product_id = product_id;
    this.sku = sku;
    this.product_title = product_title;
    ...etc...
  }



  //Class method to convert products within http response to pure array of Product objects.
  //Caller: product.service:getProducts()
  static fromJsonList(products:any): Product[] {
    let mappedArray = products.map(Product.fromJson);
    return mappedArray;
  }

  //add more parameters depending on your database entries and constructor
  static fromJson({ 
      product_id,
      sku,
      product_title,
      ...etc...
  }): Product {
    return new Product(
      product_id,
      sku,
      product_title,
      ...etc...
    );
  }
}

Here is a sample of the output I see when I load the page in Chrome. Note that on the initial load, the products are fetched from http (call to my node rest service, which is running locally on port 3000). When I then click to navigate to a 'filtered' view of the products, the products are found in cache.

My Chrome Log (console):

core.es5.js:2925 Angular is running in the development mode. Call enableProdMode() to enable the production mode.
app.component.ts:19 app.component url: /products
product.service.ts:15 product service init.  calling http to get products...
product-list.component.ts:18 product-list init...
product.service.ts:29 ** products do not yet exist; fetching from rest api...
product.service.ts:33 productResponse:  {success: true, msg: "Products found", products: Array(23)}
product-list.component.ts:20 product-list received updated products

...[clicked a menu button to filter the products]...

app.component.ts:19 app.component url: /products/chocolatechip
product-list.component.ts:18 product-list init...
product.service.ts:24 ## returning existing products
product-list.component.ts:20 product-list received updated products

Conclusion: This is the simplest way I've found (so far) to implement cacheable http response data. In my angular app, each time I navigate to a different view of the products, the product-list component reloads. ProductService seems to be a shared instance, so the local cache of 'products: Product[]' in the ProductService is retained during navigation, and subsequent calls to "GetProducts()" returns the cached value. One final note, I've read comments about how observables/subscriptions need to be closed when you're finished to prevent 'memory leaks'. I've not included this here, but it's something to keep in mind.

upvote
  flag
Note - I've since found a more powerful solution, involving RxJS BehaviorSubjects, which simplifies the code and dramatically cuts down on 'overhead'. In products.service.ts, 1. import { BehaviorSubject } from 'rxjs'; 2. change 'products:Product[]' into 'product$: BehaviorSubject<Product[]> = new BehaviorSubject<Product[]>([]);' 3. Now you can simply call the http without returning anything. http_getProducts(){this.http.get(...).map(res => res.json()).subscribe(products => this.product$.next(products))}; – ObjectiveTC
upvote
  flag
The local variable 'product$' is a behaviorSubject, which will both EMIT and STORE the latest products (from the product$.next(..) call in part 3). Now in your components, inject the service as normal. You get the most recently assigned value of product$ using productService.product$.value. Or subscribe to product$ if you want to perform an action whenever product$ receives a new value (i.e., the product$.next(...) function is called in part 3). – ObjectiveTC
upvote
  flag
Eg, in products.component.ts... this.productService.product$ .takeUntil(this.ngUnsubscribe) .subscribe((products) => {this.category); let filteredProducts = this.productService.getProductsByCategory(this.category); this.products = filteredProducts; }); – ObjectiveTC
upvote
  flag
An important note about unsubscribing from observables: ".takeUntil(this.ngUnsubscribe)". See this stack overflow question/answer, which appears to show the 'de-facto' recommended way to unsubscribe from events: //allinonescript.com/questions/38008334/… – ObjectiveTC
upvote
  flag
The alternative is to the .first() or .take(1) if the observable is only meant to receive data once. All other 'infinite streams' of observables should be unsubscribed in 'ngOnDestroy()', and if you don't then you may end up with duplicate 'observable' callbacks. //allinonescript.com/questions/28007777/… – ObjectiveTC

It's .publishReplay(1).refCount(); or .publishLast().refCount(); since Angular Http observables complete after request.

This simple class caches the result so you can subscribe to .value many times and makes only 1 request. You can also use .reload() to make new request and publish data.

You can use it like:

let res = new RestResource(() => this.http.get('inline.bundleo.js'));

res.status.subscribe((loading)=>{
    console.log('STATUS=',loading);
});

res.value.subscribe((value) => {
  console.log('VALUE=', value);
});

and the source:

export class RestResource {

  static readonly LOADING: string = 'RestResource_Loading';
  static readonly ERROR: string = 'RestResource_Error';
  static readonly IDLE: string = 'RestResource_Idle';

  public value: Observable<any>;
  public status: Observable<string>;
  private loadStatus: Observer<any>;

  private reloader: Observable<any>;
  private reloadTrigger: Observer<any>;

  constructor(requestObservableFn: () => Observable<any>) {
    this.status = Observable.create((o) => {
      this.loadStatus = o;
    });

    this.reloader = Observable.create((o: Observer<any>) => {
      this.reloadTrigger = o;
    });

    this.value = this.reloader.startWith(null).switchMap(() => {
      if (this.loadStatus) {
        this.loadStatus.next(RestResource.LOADING);
      }
      return requestObservableFn()
        .map((res) => {
          if (this.loadStatus) {
            this.loadStatus.next(RestResource.IDLE);
          }
          return res;
        }).catch((err)=>{
          if (this.loadStatus) {
            this.loadStatus.next(RestResource.ERROR);
          }
          return Observable.of(null);
        });
    }).publishReplay(1).refCount();
  }

  reload() {
    this.reloadTrigger.next(null);
  }

}

You can build simple class Cacheable<> that helps managing data retrieved from http server with multiple subscribers:

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(…)

You can have several components subscribed to it.

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

You could also use official cache mechanism.

Angular Cache

Not the answer you're looking for? Browse other questions tagged or ask your own question.