7

I have an Angular 2 app which calls a JSON API to load data into a nested HTML list (<ol>). Essentially a dynamically generated tree-view. Since the data-set from the API will eventually become very large the tree is populated progressively when the user opens a branch (user opens a branch, the API is called with the id of the node which has been opened, the API returns a JSON feed of the direct children of than node, Angular binds the returned JSON to some new HTML <li> elements and the tree expands to show the new branch).

You can see it in action in this Plunkr. It uses a recursive directive and works nicely. Currently, because I can't open the actual API up to public requests, it's just calling a static JSON feed, hence the returned data for each of the nodes is just repeated, but hopefully you get the idea.

The issue I'm now seeking to overcome is to prevent extraneous HTTP calls when a branch is closed and then re-opened. Having read the HTTP client docs I was hoping that this would be as simple as modifying the method which subscribes to the data service to chain the .distinctUntilChanged() method to the app/content-list.component.ts file, as follows:

getContentNodes() {
    this._contentService.getContentNodes(this._startNodeId)
        .distinctUntilChanged()
        .subscribe(
            contentNodes => this.contentNodes = contentNodes,
            error =>  this.errorMessage = <any>error
        );
}

However, when I open up the browser network inspector, it still makes a call to the API each time the same tree branch is re-opened.

Could anyone advise how to resolve this?

Many thanks.

EDIT:

I'm trying to implement @Thierry Templier's answer below; caching the data returned by the API. So the content service is now:

import {Injectable}                 from 'angular2/core';
import {Http, Response}             from 'angular2/http';
import {Headers, RequestOptions}    from 'angular2/http';
import {ContentNode}                from './content-node';
import {Observable}                 from 'rxjs/Observable';

@Injectable()
export class ContentService {
    constructor (private http: Http) {}

    private _contentNodesUrl = 'app/content.json';

    _cachedData: ContentNode[];

    getContentNodes (parentId:number) {
        if (this._cachedData) {
            return Observable.of(this._cachedData);
        } else {
            return this.http.get(this._contentNodesUrl + parentId)
                .map(res => <ContentNode[]> res.json())
                .do(
                    (data) => {
                    this._cachedData = data;
                })
                .catch(this.handleError);
        }
    }

    private handleError (error: Response) {
        console.error(error);
        return Observable.throw(error.json().error || 'Server error');
    }
}

However, what's happening here is that when the page loads, this._cachedData is returning false and the API call is firing and populating this._cachedData with the returned value (data), which is correct. However, when the tree node is opened in the web page UI, the following line is run repeatedly thousands of times until eventually the browser crashes:

return Observable.of(this._cachedData);

I'm still very much at the beginning when it comes to Angular 2, so would really appreciate any pointers as to why this isn't working to help my learning.

Dan
  • 5,836
  • 22
  • 86
  • 140

2 Answers2

4

I would cache the data per node using the following approach:

getData() {
  if (this.cachedData) {
    return Observable.of(this.cachedData);
  } else {
    return this.http.get(...)
          .map(res => res.json())
          .do((data) => {
            this.cachedData = data;
          });
  }
}

The "problem" with the distinctUntilChanged is that you need to use the same data flow. But it's not the case in your case since you execute several requests on different data flow...

See this question for more details:

Community
  • 1
  • 1
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • Thanks @Thierry. To clarify, should this be in the service or the component? – Dan Apr 10 '16 at 19:08
  • 2
    You're welcome! Yes exactly within your service in your getContentNodes method. – Thierry Templier Apr 10 '16 at 19:10
  • Hmm, I'm not sure if I've implemented this incorrectly as it compiles locally without error, but hangs when I open one of the tree nodes in the UI, then crashes the browser. Same with the Plunkr: http://plnkr.co/edit/Bi7buRtzczoiSyLPKoOh?p=preview – Dan Apr 10 '16 at 19:46
  • I'm not able to get this to work unfortunately. It seems to recognise that there isn't any cached data on first load, which is correct. It then populates that variable with data (`this._cachedData = data;`) successfully, but when the tree node is opened on the web page, it continually returns the observable (`return Observable.of(this._cachedData);`) thousands of times until the browser crashes. – Dan Apr 12 '16 at 17:33
  • I presume there's a problem with where/how I've initialised the `_cachedData` variable? Where/how should this be done? I've tried everything I can think of but just can not get it to work. – Dan Apr 16 '16 at 18:28
  • 2
    You should use an object for `_cachedData` that contains a list of nodes per parent id. Otherwise you also get the same data from a static resource. I updated your plunkr accordingly: http://plnkr.co/edit/fB222yP83B4DXS2MSiMr?p=preview. It seems better ;-) – Thierry Templier Apr 17 '16 at 12:22
  • 1
    Ahh, that's awesome, thanks Thierry! It makes sense now but I doubt I'd have got there :) The only tweak I've made is to cast the new object as type `any` (i.e. changed `cachedData: {string: ContentNode[]} = {};` to `cachedData: {string: ContentNode[]} = {};`) in order to prevent errors in my IDE (Visual Studio Code). Seems to work great now! :) – Dan Apr 17 '16 at 16:48
0

I guess i'm a bit late to the party, but still maybe this will help someone. A good idea is to also cache requests that are in progress. So creating two caches:

  • observableCache
  • dataCache

A use case might be a situation when we are dynamically creating components (i.e. in *ngFor) and each of those components makes a data call in its ngInit lifecycle phase. That way, even when the first request is not finished, the app won't make second request for the same data, but will subscribe to existing, cached Observable.

getData() {
  if (this.cachedData) {
    console.log('data already available');
    // if `cachedData` is available return it as `Observable`
    return Observable.of(this.cachedData);
  }
  else if (this.observableCache) {
    console.log('request pending');
    // if `this.observableCache` is set then the request is in progress
    // return the `Observable` for the ongoing request
    return this.observableCache;
  }
  else {
    console.log('send new request');
    // set observableCache as new Observable returned by fetchData method
    this.observableCache = this.fetchData();
  }
  return this.observableCache;
}

fetchData() {
  const url = String('your endpoint url');

  return this.http.get(url)
   .map(rawData => {
       // when the cached data is available we don't need the `Observable` reference
       this.observableCache = null;
       // set cachedData
       this.dataCache = new Data(rawData);

       return this.dataCache;
   })
   .share(); // critical, for multiple subscribings to the same observable
}

Full example here: https://medium.com/@garfunkel61/https-medium-com-garfunkel61-angular-data-provider-services-in-memory-data-cache-c495a0ac35da

vaindil
  • 7,536
  • 21
  • 68
  • 127
garfunkel61
  • 1,459
  • 12
  • 10