sandum
sandum

Reputation: 791

RXJS avoid repeating call if request is on progress

There is a config data in project which is used by multiple components and services. The idea is to build a service which gets just once the data from API and provides it cached every time is needed.

private getConfig(): Observable<InitConfig> {
  return this.http.get('public/config/initconfig')
    .pipe(
      tap((config: InitConfig) => {
        this.config = config; // set the config
      }),
    );
}


public getInitConfig(): Observable<InitConfig> {
  if ( _.isEmpty(this.config) ) {
    return this.getConfig();
  } else {
    return of(this.config);
  }
}

The problem is when the get request is on progress and the data is not cached yet. If another consumer calls getInitConfig() at this time I get duplicated get request.

Upvotes: 3

Views: 2149

Answers (5)

Will Alexander
Will Alexander

Reputation: 3571

Here is a slightly different approach which looks a little bit like a Redux state management system.

private loading = false;
private loaded = false;
private config$ = new BehaviorSubject<InitConfig>(null);

public getConfig() {
  this.requestConfig();
  return this.config$.asObservable();
}

public requestConfig() {
  if (loading || loaded) {
    return;
  }
  this.loading = true;
  this.http.get('public/config/initconfig')
    .pipe(
      take(1),
      tap((config: InitConfig) => {
        this.config$.next(config);
        this.loaded = true;
        this.loading = false;
      }),
    ).subscribe();
}

It may be worth looking into implementing something like NgRx, as it is built precisely for this kind of more complex state management.

Upvotes: 0

micronyks
micronyks

Reputation: 55443

The usual problem with your approach is, whenever you make an HTTP call, its going to give you a new observable. This new observable is by default COLD in nature . It means you have to subscribe to it to get the result. If you call it multiple times, you are gonna get multiple observables and you will end up making multiple HTTP requests.

SOLUTION

You should use a single Observable and subscribe to it multiple times with Share operator. Share operator will make the observable HOT after making the first HTTP call. As soon as it becomes hot, if other subscriber is found, rather than making an other HTTP call, it will simply return the data.

in service,

private publicObservable = this.getConfig();          // publicObservable single observable  

private getConfig(): Observable<InitConfig> {
  return this.http.get('public/config/initconfig')
    .pipe(
        share()                                      // share operator
    );
}


public getInitConfig(): Observable<InitConfig> {
     return this.service.publicObservable;
}

In Demo, I subscribe to same HTTP 8 times. If I remove, share operator, you can see in NETWORK tab, 8 calls will be made

With share operator, only 1 request will be made.

DEMO

I hope this will help !

Upvotes: 4

Tal Ohana
Tal Ohana

Reputation: 1138

You need a private observable that indicates if the config was already fetched, if it wasn't (which means it's the first subscription) initialize it and either way return it.

First, add a private config$: Observable<InitConfig> to your service.
Next, use shareReplay(1) in order to share the result among the subscribers.

It should look something like this:

  private config$: Observable<InitConfig>;

  getInitConfig(): Observable<InitConfig> {
    if (!this.config$) {
      this.config$ = this.http
        .get("https://jsonplaceholder.typicode.com/users/1")
        .pipe(
          tap(() => console.log("config fetched")),
          shareReplay(1)
        );
    }

    return this.config$;
  }

You can test it by subscribing multiple times and see config fetched being logged only once:

  ngOnInit(): void {
    this.configService.getInitConfig().subscribe(() => console.log('Config Subscription - 1'))
    this.configService.getInitConfig().subscribe(() => console.log('Config Subscription - 2'))
    this.configService.getInitConfig().subscribe(() => console.log('Config Subscription - 3'))
  }

You can see the full code in this stackblitz

Upvotes: 4

enno.void
enno.void

Reputation: 6579

You can do this with shareReplay(1)

private getConfig(): Observable<InitConfig> {
  return this.http.get('public/config/initconfig')
    .pipe(
      shareReplay(1)
    );
}

Example: https://stackblitz.com/edit/angular-ivy-b2osdk

Upvotes: -1

Andrei
Andrei

Reputation: 11951

promises will work exactly as you want. it is the most easy and elegant way

private getConfig(): Promise<InitConfig> {
  return this.http.get('public/config/initconfig').toPromise();
}


public getInitConfig(): Promise<InitConfig> {
  return this.config = this.config || this.getConfig();
}

Upvotes: 0

Related Questions