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.