2

How do I store a local variable or cache it so that the results of an API call can be shared throughout the application?

I have the following Angular 6 module which basically creates a mentions directive to be able to do @mentions popular in many apps:

@NgModule({
    imports: [
        CommonModule
    ],
    exports: [
        MentionDirective,
        MentionListComponent
    ],
    entryComponents: [
        MentionListComponent
    ],
    declarations: [
        MentionDirective,
        MentionListComponent
    ]
})
export class MentionModule {
    static forRoot(): ModuleWithProviders {
        return {
            ngModule: MentionModule
        };
    }
}

This is the actual directive:

export class MentionDirective implements OnInit, OnChanges {
items: any[];

constructor(
  public autocompletePhrasesService: AutocompletePhrasesService
) { }

  ngOnInit() {
    this.autocompletePhrasesService.getAll()
      .pipe(first())
      .subscribe(
        result => {
          this.items = result;
        },
        () => { });

The directive calls a function in my core module which retrieves the data:

export class AutocompletePhrasesService {

    public autoCompletePhrases: string[];

    constructor(private http: HttpClient) { }

    getAll(): Observable<string[]> {
        return this.http.get<string[]>(this.baseUrl + '/getall');
    }

My problem is that this directive may have 20-30 instances on a single page and the API gets called for every instance of the directive. How can I change my code so that the API gets called just once per application instance?

The data does not change often from the API.

I have tried storing the results of the service in the public autoCompletePhrases variable, and only calling that if it is empty, but that hasn't worked

Robbie Mills
  • 2,705
  • 7
  • 52
  • 87
  • Use a service and inject it into the components which need it. – Dale K Dec 05 '18 at 07:46
  • I *am* using a service (AutocompletePhrasesService) and I *am* injecting it into the one component that needs it (MentionDirective). I'm not trying to share data across components, I'm sharing data against multiple instances of the same component – Robbie Mills Dec 05 '18 at 07:48
  • you can store the data in the localstorage – Hana Wujira Dec 05 '18 at 07:50
  • Yes, I can, but that's a per user setting, I have thousands of users so thought there must be a better way – Robbie Mills Dec 05 '18 at 07:51
  • Yes, but a service is single instance per app whereas you component has multiple instances. So if you use the service to call the API and then cache it for your multiple component instances. Services are for more than sharing data :) – Dale K Dec 05 '18 at 07:51
  • Ok that makes sense @DaleBurrell but I must be doing something wrong. My AutoCompletePhrases service is in a core module (so only called once) and it is what calls the API, how do I cache the response? – Robbie Mills Dec 05 '18 at 07:52
  • 1
    A simple example https://stackoverflow.com/questions/44638392/how-can-i-cache-some-data-in-angular-4-service but in my experience you'll also need to cope with the loading case i.e. the first calls may occur while the data is being retrieved so there is a third condition, not loading, loading, data available. – Dale K Dec 05 '18 at 07:54
  • 1
    https://stackoverflow.com/questions/36271899/what-is-the-correct-way-to-share-the-result-of-an-angular-http-network-call-in-r – Dale K Dec 05 '18 at 07:55
  • 1
    this might be a case where you consider using ngrx to share the data across your components. This is a mechanism for sharing state in Angular – jazza1000 Dec 05 '18 at 08:13

2 Answers2

1

You want some like "cache". Its easy in service make some like

data:any;
getData()
{
    if (!this.data)
      return this.httpClient.get(....).pipe(
        tap(result=>{
            this.data=result
        }))
    return of(this.data)
}

When you subscribe to "getData" can happen two things: this.data has value, then return an observable with the data. Or this data has no value, and return a get, and store the result in data -tap is executed after subscribe-. So you ALLWAYS subscribe to "getData", but the service only call to API if has not data (NOTE: can happens that there're more that one call at time and the call to API more than one time)

Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • You also need to handle the loading case, you don't want to send off another request if you've already sent the first but not received the result. – Dale K Dec 05 '18 at 08:11
  • Thanks, this looks good but when I implemented I still get 20 requests! I suspect because of the loading case that @DaleBurrell discussed, how do I get around that? – Robbie Mills Dec 05 '18 at 08:14
  • You can use APP_INITIALIZER to get all the data at first https://angular.io/api/core/APP_INITIALIZER – Eliseo Dec 05 '18 at 08:42
1

Here is some AngularJS/TypeScript code to demonstrate the logic of how to handle this. Apologies but I don't have time to convert it to Angular. I do believe there are also packages out there which handle caching in a more complete fashion but this is the manual way to do it :)

private printerList: SelectItem[] = [];
private printerPromises: IDeferred<SelectItem[]>[] = [];
private printersLoading: boolean = false;
private printersLoaded: boolean = false;

public getPDFPrinterList(): ng.IPromise<SelectItem[]> {
  let defer: IDeferred<SelectItem[]> = this.$q.defer<SelectItem[]>();

  this.printerPromises.push(defer);

  if (this.printersLoading) {
      return defer.promise;
  }

  if (this.printersLoaded) {
      for (let i = 0; i < this.printerPromises.length; i++) {
          this.printerPromises[i].resolve(this.printerList);
      }
      this.printerPromises = [];
  } else {
      this.printersLoading = true;
      this.$http.get<SelectItem[]>(this.printerListUrl, { cache: false }).then((response: ng.IHttpResponse<SelectItem[]>) => {
          if (response.data) {
              this.printerList = response.data;
              for (let i = 0; i < this.printerPromises.length; i++) {
                  this.printerPromises[i].resolve(this.printerList);
              }
              this.printerPromises = [];
              this.printersLoaded = true;
          } else {
              console.log('Server Error Obtaining Printer List');
              for (let i = 0; i < this.printerPromises.length; i++) {
                  this.printerPromises[i].reject('Server Error');
              }
              this.printerPromises = [];
          }
          this.printersLoading = false;
      }, (response: ng.IHttpResponse<any>) => {
          if (response.data) {
             console.log(response.data.Message ? response.data.Message : response.data);
          }
          for (let i = 0; i < this.printerPromises.length; i++) {
              this.printerPromises[i].reject('Server Error');
          }
          this.printerPromises = [];
      });
  }

  return defer.promise;
}
Dale K
  • 25,246
  • 15
  • 42
  • 71