Search code examples
angulartypescriptrxjsobservableangular-services

Share data retrieved using HttpClient between Angular components


I would like to create a service method which at first execution will fetch a dataset from an URL, cache the dataset, and then share the same instance of the dataset with the components which subsequently call the service method.

I have started creating two solutions. The problem with both solutions is that in the first milliseconds after loading the Angular app, they fetch the dataset from the URL multiple times, and/or they throw some error messages to the console, but after the first or second navigation between the two components, the data seems to be shared correctly, and there is no more roundtrip to the URL.

Approach #1

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class FirstService {
  private url = 'http://somedummydomain.com/api/entries';
  private data: any;
  private observable: Observable<any>;

  constructor(private http: HttpClient) {}

  getData() : Observable<any> {
    if (this.data) {
      return of(this.data);
    } else if (this.observable) {
      return this.observable;
    } else {
      this.observable = this.http.get(this.url);
      this.observable.subscribe((data) => {
        this.data = data;
        this.observable = null;
      });
      return this.observable;
    }
  }
}

Approach #2

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root',
})
export class SecondService {
  private sharedData: BehaviorSubject<any> = new BehaviorSubject<any>(null);
  private url = 'http://somedummydomain.com/api/entries';

  constructor(private http: HttpClient) {
    this.http.get(this.url).subscribe((data) => {
      this.addData(data);
    });
  }

  private addData(data: any) {
    this.sharedData.next(data);
  }

  getData(): Observable<any> {
    return this.sharedData.asObservable();
  }
}

Lib versions

  • angular: v9.1.9
  • rxjs: v6.5.5

Thanks!


Solution

  • For completeness, here is the reactive style of solving your requirement (sharing data across subscribers):

    @Injectable({
      providedIn: 'root',
    })
    export class SecondService {
      private url = 'http://somedummydomain.com/api/entries';
      private data$: Observable<any>;
    
      constructor(private http: HttpClient) {
        this.data$ = this.http.get(this.url).pipe(
          shareReplay(1)
        );
      }
    
      getData(): Observable<any> {
        return this.data$;
      }
    }
    

    The shareReplay operator will do two things:

    1. It will share the Observable - that means that all subscribers will receive the same notifications and the source is subscribed to only once. Approach 1 fails for you, because each subscriber will trigger HTTP GET again until the first request completes and you get to your this.data !== undefined branch.
    2. It will replay the last N notifications (in this case only the last notification) to all subscribers that came too late to the party - i.e. that subscribed after the HTTP call already returned a value. If you don't need this, use share instead.

    If you need data as a property, you can set it with a tap operator like this:

    this.data$ = this.http.get(this.url).pipe(
      tap(data => this.data = data),
      shareReplay(1)
    );