By using Http, we call a method that does a network call and returns an http observable:
getCustomer() {
return
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
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.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.
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;
}
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,
onnext
.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.
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());