caching results with angular2 http service

AngularTypescript

Angular Problem Overview


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

Angular Solutions


Solution 1 - Angular

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)

Solution 2 - Angular

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

Solution 3 - Angular

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSagi View Question on Stackoverflow
Solution 1 - AngulartibbusView Answer on Stackoverflow
Solution 2 - AngularGünter ZöchbauerView Answer on Stackoverflow
Solution 3 - AngularEric MartinezView Answer on Stackoverflow