1
votes

Can I use one-time subscription to http action inside an Angular service? Is there any disadvantage of doing so?

public async signInAsync(userName: string, password: string): Promise<void> {
    try {
      const token = await this.authenticate(userName, password).pipe(first()).toPromise();
      this.signInCompleted(token);
    } catch {
      this.signInFailed();
    }
  }

In order to use OnPush change detection strategy and also keep "business logic" inside services, services use observable called state$ to expose values to components Components then subscribes to this value with async pipe. Only functions of service are able to update the state by calling this.patchState('actionName', {...}).

protected signInCompleted(token: string): void {
    this.localStorageService.setItem(LocalStorageKey.AUTH_TOKEN, token);
    this.patchState('signInCompleted', {
      isAuth: true,
      token: token,
      error: null
    });
    this.router.navigate([AppRoute.AUTH]);
  }

Therefore, if I use HttpClient, I have to somehow subscribe to returned observable.

I've started with simple subscription:

protected signIn(...): void {
    this.authenticate(..).subscribe(..);
}

But then, I've realized that it's not testable, because I don't know when the call is executed and async() doesn't know about observable. To keep it testable, I had to make it async and convert to promise, however I'm not sure if there is any disadvantage if I subscribe with pipe(first()).toPromise().

I was also thinking about using pipe(map(...)).pipe(catchError(...)), but I don't know how then bind the action to component, or whether its better then the previous approach.

  public signIn(userName: string, password: string): Observable<void> {
    return this.authenticate(userName, password).pipe(map(...)).pipe(catchError(...));
  }
1
What value do you want to wait for in your component after you call signInAsync? What should be returned to your component? You should usually be able to do everything with Observables only.frido
The idea was to create something like redux effect => async callback should run action and update state (therefore, no value should be returned to the component, value will be saved in store). However, mostly I've tried to fetch values from server when component is loaded, but keep them in shared state. It seems that second alternative using pipe(map()) works better, mostly because it is easier to cancel calls when I use observables.bojo
Yes the second approach is better. You also don't have to use multiple pipe you can just add all the operators to one pipe. To perform side effect like updating a shared state you can use the tap operator. this.authenticate(userName, password).pipe(tap(...), catchError(...));frido
I end up with a custom operator, that uses single pipe and tap as you said, so that's good to hear, thank you. export function effect<T>( completed?: (value: T) => void, failed?: (error: any) => void ): OperatorFunction<T, void> { return (observable$: Observable<T>): Observable<void> => observable$.pipe( tap(completed, failed), catchError(_ => of()), map(() => {}) ); }bojo

1 Answers

0
votes

It seems that using promises is not a good approach, while using observables is easy to cancel calls when newer request is needed or component was destroyed. I have to say that syntax is not that clear as with async/await in promises, and also it is also harder to test sometimes, but cancellation is very useful (especially if you have API calls for search in autocomplete component bind to every keystroke and so on).

For a side-effect handling, I suggest to create a new operator:

export function effect<T>(
  completed?: (value: T) => void,
  failed?: (error: any) => void
): OperatorFunction<T, void> {
  return (observable$: Observable<T>): Observable<void> =>
    observable$.pipe(
      tap(completed, failed),
      catchError(_ => of()),
      map(() => {})
    );
}

use it in a service:

public login(userName: string, password: string): Observable<void> {
    return this.loginUsingPOST(userName, password).pipe(
      effect(
        token => this.loginCompleted(token),
        error => this.loginFailed(error)
      )
    );
  }

and subscribe in components only:

  public submit(): void {
    this.authService
      .login(this.loginForm.value.userName, this.loginForm.value.password)
      .pipe(this.untilDestroy())
      .subscribe();
  }

Thanks to @fridoo