What is the correct way to share the result of an Angular Http network call in RxJs 5?
Asked Answered
D

22

355

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?

Note : As per Angular 5 new HttpClient, the .map(res => res.json()) part in all examples is now useless, as JSON result is now assumed by default.

Deplorable answered 28/3, 2016 at 21:55 Comment(6)
> share is identical to publish().refCount(). Actually it's not. See the following discussion: github.com/ReactiveX/rxjs/issues/1363Whorehouse
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.tsDeplorable
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.Cost
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?Canoe
You could simply use ngx-cacheable! It better suits your scenario. Refer my answer belowIodism
Please consider to up vote the answer of @Arlo. At least when I'm working with multiple subscribes and combineLatest(). All I need to do is add shareReplay(1) as last operator in my pipe and requests are shared inside a single 'call stack'.Deluxe
B
245

EDIT: as of 2021, the proper way is to use the shareReplay operator natively proposed by RxJs. See more details in below answers.


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

This article https://blog.thoughtram.io/angular/2018/03/05/advanced-caching-with-rxjs.html is a great explanation how to cache with shareReplay.

Blowpipe answered 29/3, 2016 at 17:56 Comment(23)
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)?Thiouracil
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.Penury
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!Thiouracil
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.Penury
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?Thiouracil
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.Penury
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(...[stuff]...).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!Mango
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.Bradstreet
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.Penury
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.Bradstreet
Is this method still compatible with latest angular version 2.1.0 after the release of the stable version?Shivaree
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 ;-)Penury
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.Orlantha
@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; }Indicative
import {Data} from './data'; What is data and why this was used?Prenotion
@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.Penury
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?Barbate
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.Penury
Works now in angular 6 and RXjs 6 with very minimal changes, an answer that stood the tests of time even with the Javascript world.Rejuvenate
Great, thanks. I worked but I had to change return Observable.of(this.data); to return of(this.data);. What do you mean by "proper way to import the 'of' operator"?Debauched
@Debauched I haven't used rxjs 6.0 and I don't remember what that comment was about. It's a long time ago :-/Penury
I think it's a good way to demonstrate and learn the actual problem and steps involved, but shareReplay is usually just way more convenient blog.thoughtram.io/angular/2018/03/05/…Penury
I don't know why, but my lighthouse performance went down when i implemented this sharePlay codeHence
D
51

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();
}
Deplorable answered 29/3, 2016 at 22:9 Comment(3)
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/…Whorehouse
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"Whorehouse
@Whorehouse Hey, can you explain what you mean by saying "cannot be cancelled or retried"? Thanks.Wonted
S
40

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")
Saied answered 23/3, 2017 at 1:28 Comment(2)
Is there any reason to not to use this solution as an RxJs operator: const data$ = this._http.get('url').pipe(cacheable()); /*1st subscribe*/ data$.subscribe(); /*2nd subscribe*/ data$.subscribe();? So it behaves more like any other operator..Valero
I get Type 'Observable<any>' provides no match for the signature '(): Observable<unknown>'. in the last solution.Orban
S
37

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.

Shrug answered 12/5, 2017 at 17:15 Comment(3)
Is it related to this? These docs are from 2014 though. github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/…Alcine
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?Foldaway
Looking at the changelog github.com/ReactiveX/rxjs/blob/… It appeared earlier, was removed in v5, added back in 5.4 - that rx-book link does refer to v4, but it exists in the current LTS v5.5.6 and it's in v6. I imagine the rx-book link there is out of date.Will
U
31

rxjs version 5.4.0 (2017-05-09) adds support for shareReplay.

Why use shareReplay?

You generally want to use shareReplay when you have side-effects or taxing computations that you do not wish to be executed amongst multiple subscribers. It may also be valuable in situations where you know you will have late subscribers to a stream that need access to previously emitted values. This ability to replay values on subscription is what differentiates share and shareReplay.

You could easily modify an angular service to use this and return an observable with a cached result that will only ever make the http call a single time (assuming the 1st call was successfull).

Example Angular Service

Here is a very simple customer service that uses shareReplay.

customer.service.ts

import { shareReplay } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';

@Injectable({providedIn: 'root'})
export class CustomerService {

    private readonly _getCustomers: Observable<ICustomer[]>;

    constructor(private readonly http: HttpClient) {
        this._getCustomers = this.http.get<ICustomer[]>('/api/customers/').pipe(shareReplay());
    }
    
    getCustomers() : Observable<ICustomer[]> {
        return this._getCustomers;
    }
}

export interface ICustomer {
  /* ICustomer interface fields defined here */
}

Note that the assignment in the constructor could be moved to the method getCustomers but as observables returned from HttpClient are "cold" doing this in the constructor is acceptable as the http call will only every be made with the first call to subscribe.

Also the assumption here is that the initial returned data does not get stale in the lifetime of the application instance.

Unship answered 28/2, 2019 at 20:53 Comment(3)
I really like this pattern and am looking to implement it within a shared library of api services I use across a number of applications. One example is a UserService, and everywhere except a couple places don't need to invalidate the cache during the lifetime of the app, but for those cases, how would I go about invalidating it without causing previous subscriptions to become orphaned?Blimp
If we move creation of Observable in the constructor to the method getCustomer, then different components calling getCustomer will receive different observable instaces. That might not be what we want. so I believe creation of observable should be in constructor. If we are okay that different calls to getCustomer() should return different observables then its okay to have in the method itself.Caterina
shareReplay with a window lets the cache get stale, right?Tumer
I
30

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

Immediately answered 10/6, 2016 at 20:17 Comment(0)
K
10

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.

Kinnon answered 29/3, 2016 at 21:59 Comment(1)
I like this approach as it makes good use of rxjs and no need to add custom logic, thank-youSuppose
G
8

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.

------ edit -------------
If you want to have refreshed data with F5, which usesmemory data instead of sessionStorage;

@Injectable()
export class HttpCache {
  cached: any = {};  // this will store data
  constructor(private http: Http) {}

  get(url: string): Observable<any> {
    if (this.cached[url]) {
      return Observable.of(this.cached[url]));
    } else {
      return this.http.get(url)
        .map(resp => {
          this.cached[url] = resp.text();
          return resp.json();
        });
    }
  }
}
Grounder answered 26/7, 2016 at 22:25 Comment(4)
If you will store in session Storage then How will you make sure that Session storage is destroyed when you leave the app ?Pendulum
but this introduces unexpected behavior for the user. When the user hits F5 or refresh button of the browser, then he expects fresh data from server. But actually he is getting outdated data from localStorage. Bug reports, support tickets, etc. incoming... As the name sessionStorage says, I would use it only for data that is expected to be consistent for the whole session.Misbehave
@MA-Maddin as I stated "I used it to avoid usage limit". If you want want data to be refreshed with F5, you need to use memory instead of sessionStorage. The answer has been edited with this approach.Grounder
yep, that might be a use case. I just got triggered since everyone is talking about Cache and OP has getCustomer in his example. ;) So just wanted to warn some ppl that might do not see the risks :)Misbehave
S
7

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;
}
Shrug answered 9/5, 2017 at 20:38 Comment(1)
Hi @Shrug - the example above does not compile. Property 'connect' does not exist on type '{}'. from the line returnValue.connect();. Can you elaborate?Barbate
K
5

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.

Kannry answered 2/9, 2017 at 7:46 Comment(5)
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))};Kannry
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).Kannry
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; });Kannry
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: #38008834Kannry
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. #28008277Kannry
C
3

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:

Conney answered 3/5, 2017 at 7:3 Comment(0)
C
3

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

My personal favourite is to make use of async methods for calls that make network requests. The methods themselves don't return a value, instead they update a BehaviorSubject within the same service, which components will subscribe to.

Now Why use a BehaviorSubject instead of an Observable? Because,

  • Upon subscription BehaviorSubject returns the last value whereas A regular observable only triggers when it receives an onnext.
  • If you want to retrieve the last value of the BehaviorSubject in a non-observable code (without a subscription), you can use the getValue() method.

Example:

customer.service.ts

public customers$: BehaviorSubject<Customer[]> = new BehaviorSubject([]);

public async getCustomers(): Promise<void> {
    let customers = await this.httpClient.post<LogEntry[]>(this.endPoint, criteria).toPromise();
    if (customers) 
        this.customers$.next(customers);
}

Then, wherever required, we can just subscribe to customers$.

public ngOnInit(): void {
    this.customerService.customers$
    .subscribe((customers: Customer[]) => this.customerList = customers);
}

Or maybe you want to use it directly in a template

<li *ngFor="let customer of customerService.customers$ | async"> ... </li>

So now, until you make another call to getCustomers, the data is retained in the customers$ BehaviorSubject.

So what if you want to refresh this data? just make a call to getCustomers()

public async refresh(): Promise<void> {
    try {
      await this.customerService.getCustomers();
    } 
    catch (e) {
      // request failed, handle exception
      console.error(e);
    }
}

Using this method, we don't have to explicitly retain the data between subsequent network calls as it's handled by the BehaviorSubject.

PS: Usually when a component gets destroyed it's a good practice to get rid of the subscriptions, for that you can use the method suggested in this answer.

Coranto answered 24/3, 2018 at 1:36 Comment(0)
H
3

Great answers.

Or you could do this:

This is from latest version of rxjs. I am using 5.5.7 version of RxJS

import {share} from "rxjs/operators";

this.http.get('/someUrl').pipe(share());
Hellhole answered 29/3, 2018 at 15:54 Comment(0)
P
2

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

Petro answered 27/11, 2017 at 4:26 Comment(0)
S
1

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.

Shrug answered 22/4, 2017 at 0:26 Comment(0)
G
1

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);
  }

}
Gobble answered 14/11, 2017 at 22:40 Comment(0)
G
1

Most of the answers above are fine for http requests which doesn't take input. Every time you want to make an api call using some input, the request needs to be created anew. The only response above which could handle this, is @Arlo's reply.

I've created a slightly simpler decorator you can use to share the response to every caller which has the same input. Unlike Arlo's reply, this does not replay responses to delayed subscribers, but will handle simultaneous requests as one. If the goal is to replay responses to delayed observers (aka cached responses), you can modify the code below and replace share() with shareReplay(1):

https://gist.github.com/OysteinAmundsen/b97a2359292463feb8c0e2270ed6695a

import { finalize, Observable, share } from 'rxjs';

export function SharedObservable(): MethodDecorator {
  const obs$ = new Map<string, Observable<any>>();
  return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const key = JSON.stringify(args);
      if (!obs$.has(key)) {
        // We have no observable for this key yet, so we create one
        const res = originalMethod.apply(this, args).pipe(
          share(), // Make the observable hot
          finalize(() => obs$.delete(key)) // Cleanup when observable is complete
        );
        obs$.set(key, res);
      }
      // Return the cached observable
      return obs$.get(key);
    };
    return descriptor;
  };
}

USAGE:

@SharedObservable()
myFunc(id: number): Observable<any> {
  return this.http.get<any>(`/api/someUrl/${id}`);
}
Galloot answered 7/4, 2022 at 10:48 Comment(0)
P
0

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;
   });
  }

}
Pathological answered 23/5, 2016 at 9:43 Comment(0)
D
0

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.

Desmarais answered 16/12, 2016 at 14:40 Comment(0)
L
0

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]);
                     }
                     }
                 );
             }
            });
        }
}
Lugansk answered 23/1, 2017 at 10:52 Comment(0)
I
0

You could simply use ngx-cacheable! It better suits your scenario.

The benefit of using this

  • It calls rest API only once, caches the response & returns the same for following requests.
  • Can call API as required after create/ update/ delete operation.

So, Your service class would be something like this -

import { Injectable } from '@angular/core';
import { Cacheable, CacheBuster } from 'ngx-cacheable';

const customerNotifier = new Subject();

@Injectable()
export class customersService {

    // relieves all its caches when any new value is emitted in the stream using notifier
    @Cacheable({
        cacheBusterObserver: customerNotifier,
        async: true
    })
    getCustomer() {
        return this.http.get('/someUrl').map(res => res.json());
    }

    // notifies the observer to refresh the data
    @CacheBuster({
        cacheBusterNotifier: customerNotifier
    })
    addCustomer() {
        // some code
    }

    // notifies the observer to refresh the data
    @CacheBuster({
        cacheBusterNotifier: customerNotifier
    })
    updateCustomer() {
        // some code
    }
}

Here's the link for more reference.

Iodism answered 19/11, 2018 at 14:26 Comment(0)
E
-5

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
// ...
Enrollee answered 29/3, 2016 at 13:48 Comment(1)
i've edit the question to make it Angular 2 specificDeplorable

© 2022 - 2024 — McMap. All rights reserved.