RXJS avoid repeating call if request is on progress

Viewed 944

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.

5 Answers

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

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 !

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

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.

Related