I expose an HTTP GET request through a service, and several components are using this data (profile details on a user). I would like the first component request to actually perform the HTTP GET request to the server and cache the results so the the consequent requests will use the cached data, instead of calling the server again.
Here's an example to the service, how would you recommend implementing this cache layer with Angular2 and typescript.
import {Inject, Injectable} from 'angular2/core';
import {Http, Headers} from "angular2/http";
import {JsonHeaders} from "./BaseHeaders";
import {ProfileDetails} from "../models/profileDetails";
@Injectable()
export class ProfileService{
myProfileDetails: ProfileDetails = null;
constructor(private http:Http) {
}
getUserProfile(userId:number) {
return this.http.get('/users/' + userId + '/profile/', {
headers: headers
})
.map(response => {
if(response.status==400) {
return "FAILURE";
} else if(response.status == 200) {
this.myProfileDetails = new ProfileDetails(response.json());
return this.myProfileDetails;
}
});
}
}
The share() operator works just on the first request, when all the subscriptions are served and you create another one, then it will not work, it will make another Request. (this case is pretty common, as for the angular2 SPA you always create/destroy components)
I used a ReplaySubject to store the value from the http observable. The ReplaySubject observable can serve previous value to its subscribers.
the Service:
@Injectable()
export class DataService {
private dataObs$ = new ReplaySubject(1);
constructor(private http: HttpClient) { }
getData(forceRefresh?: boolean) {
// If the Subject was NOT subscribed before OR if forceRefresh is requested
if (!this.dataObs$.observers.length || forceRefresh) {
this.http.get('http://jsonplaceholder.typicode.com/posts/2').subscribe(
data => this.dataObs$.next(data),
error => {
this.dataObs$.error(error);
// Recreate the Observable as after Error we cannot emit data anymore
this.dataObs$ = new ReplaySubject(1);
}
);
}
return this.dataObs$;
}
}
the Component:
@Component({
selector: 'my-app',
template: `<div (click)="getData()">getData from AppComponent</div>`
})
export class AppComponent {
constructor(private dataService: DataService) {}
getData() {
this.dataService.getData().subscribe(
requestData => {
console.log('ChildComponent', requestData);
},
// handle the error, otherwise will break the Observable
error => console.log(error)
);
}
}
}
fully working PLUNKER
(observe the console and the Network Tab)
I omitted the userId
handling. It would require to manage an array of data
and an array of observable
(one for each requested userId
) instead.
import {Injectable} from '@angular/core';
import {Http, Headers} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/observable/of';
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
You can find another interesting solution at https://stackoverflow.com/a/36296015/217408
Regarding your last comment, this is the easiest way I can think of : Create a service that will have one property and that property will hold the request.
class Service {
_data;
get data() {
return this._data;
}
set data(value) {
this._data = value;
}
}
As simple as that. Everything else in the plnkr would be untouched. I removed the request from the Service because it will be instantiated automatically (we don't do new Service...
, and I'm not aware of an easy way to pass a parameter through the constructor).
So, now, we have the Service, what we do now is make the request in our component and assign it to the Service variable data
class App {
constructor(http: Http, svc: Service) {
// Some dynamic id
let someDynamicId = 2;
// Use the dynamic id in the request
svc.data = http.get('http://someUrl/someId/'+someDynamicId).share();
// Subscribe to the result
svc.data.subscribe((result) => {
/* Do something with the result */
});
}
}
Remember that our Service instance is the same one for every component, so when we assign a value to data
it will be reflected in every component.
Here's the plnkr with a working example.
Reference
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With