Search code examples
angularrxjsngrxangular-httpclient

RxJS Update State and Passthrough


I have a user library that is used across several Angular apps in my organization. As a refactor/new version, we are looking to move towards a state-based service, with an interface with something like this:

export interface UserService {
    user: Observable<User>
    getProfile(url: string): Observable<User>
    updateProfile(url: string): Observable<User>
}

This way, consumers can bind components to UserService.user knowing that the data will always be up-to-date, and call get/update functions in buttons or background activity, etc.

Clarification: The user library is meant to be used in apps that use ngRx, as well as those that do not want something as heavy, but still want observable-based user binding.

So I wire up the functions like this (with slight variations) :

public getProfile(url: string): Observable<User> {
    const obs = this._http.get<User>(url);
    obs.subscribe(profile => this.setProfile(profile));
    return obs;
}

Where setProfile updates an internal Subscription that UserService.user returns in a read-only manner.

Once of my consumer apps uses ngRx for the rest of it's state management. When consuming my library, we notice some oddities:

  1. When wrapped inside of an ngRx effect, sometimes any functions using Angular's HttpClient will be called several times. To rectify this issue, any function I have using HttpClient ends with .publishLast().refCount() to ensure the HTTP call is made only once.

  2. If a call fails, the consumer cannot catch the exception. For instance, this.authService.getProfile().catch(error => alert(error)) will never get called. To get around this, I am now modifying my functions on their subscribe: obs.subscribe(profile => this.setProfile(profile), error => Observable.throw(error));

Is this normal behavior when implementing a "state-store" service in Angular?

Edit: Sample ngRx effect (please note that this is real implementation, what I posted above is way simplified from our actual implementation):

public BiographyUpdate: Observable<any> = this._actions.pipe(
    ofType(AuthActionTypes.BIOGRAPHY_UPDATE),
    map((action: BiographyUpdate) => action.payload),
    switchMap(bio => this._auth.update(`${Environment.Endpoints.Users}/users/sync/biography`, action)
      .map(profile => new BiographyUpdateSuccess(profile.biography))
      .catch(error => {
        console.log("AM I BEING HIT?");
        return Observable.of(new BiographyUpdateFailure(error.toString()))
      })
    )
  );

Solution

  • Once you call the subscribe event, it is fired and the result resolved (correct result or error) inside the subscribe method. So at the time, you call this.authService.getProfile().catch method, the asynchronous event is already fired and finished. That is why it's not calling.

    You can either move the catch operator in the getProfile or you can return the observable from getProfile without subscribing to it.

    public getProfile(url: string): Observable<User> {
        return this._http.get<User>(url)
                .pipe( tap( profile => this.setProfile(profile))); 
    }
    

    Now subscribe it through the authService service

    this.authService.getProfile()
      .subscribe(profile =>  // do the work)
      .catch(error => alert(error))