Daniel
Daniel

Reputation: 163

RxJS - Two events that can change one state

I arrived at this question with the following scenario:

I have a service that does Http calls to an API, it needs to access the user data to set the auth header.

this below is the function that returns the observable to use in the template

getRelatorioCobranca(date: Date) {
    return this.auth.userObs$.pipe(
      switchMap((userData) => {
        const header = this.utils.createAuthHeader(userData);

        return this.http.get<RelatorioCobranca>(
          `${
            environment.apiBase
          }/relatorios/cobrancas?${this.utils.monthYearQuery(date)}`,
          { headers: header }
        );
      })
    );
  }

as you can see this function takes a date parameter so I created a Date BehaviorSubject so I can switchMap into this function and get the data.

 relatorioCobranca$ = this.selectedDate$.pipe(
    tap(() => {
      this.loadingResults = true;
    }),
    switchMap((date) => {
      return this.relatorios.getRelatorioCobranca(date);
    }),
    tap(() => {
      this.loadingResults = false;
    })
  );

it all works fine, the data is reactive to the selectedDate and also to any changes in the User observable. the issue arises when I try to implement some sort of loading state.

I tried using tap before the switchMap to set a boolean to true to set a loading state but this only works if the 'selectedDate$' emits (If I change the date), If the user observable emits then the first tap isn't executed, only the last one which doesn't create the loading effect.

Is there any way to make so that both events set the flag to true in this current design? Or should I make this some other way? Removing the switchMap from the service function probably would solve the issue but now in every component I would need to have something like this:

relatorioCobranca$ = this.auth.userObs$.pipe(
    tap(() => this.loadingResults = true),
    switchMap((userData) => {
      return this.selectedDate$.pipe(
        tap(() => this.loadingResults = true),
        switchMap((date) => {
          return this.relatorios.getRelatorioCobranca(date);
        })
      )
    }),
    tap(() => this.loadingResults = false),
  );

which I don't think would be great either, since this behavior is present in multiple components, but maybe this is the right way?

Obs: am using async pipe to subscribe in the template.

Upvotes: 0

Views: 60

Answers (1)

JSmart523
JSmart523

Reputation: 2497

I'd use combineLatest() and set it up so whether or not it's loading is an observable as well, like so...


setUpInput$ = o$ => concat(
  of(undefined),
  o$
).pipe(
  distinctUntilChanged()
);

relatorioCobranca_newInputs$ = combineLatest(
  setUpInput$(this.auth.userObs$),
  setUpInput$(selectedDate$)
).pipe(
  filter(
    ([userObs, selectedDate]) => userObs !== undefined && selectedDate !== undefined
  )
);

relatorioCobranca$ = relatorioCobranca_newInputs$.pipe(
  switchMap(([userData, date])) => this.http.get<RelatorioCobranca>(
    `${environment.apiBase}/relatorios/cobrancas?${this.utils.monthYearQuery(date)}`,
    {
      headers: this.utils.createAuthHeader(userData)
    }
  ))
);

relatorioCobranca_Loading$: Observable<boolean> = merge(
  of(false),
  relatorioCobranca_newInputs$.pipe(
    mapTo(true)
  ),
  relatorioCobranca$.pipe(
    mapTo(false)
  )
).pipe(
  shareReplay(1)
);

This way, as soon as you have both a date and user data, relatorioCobranca_Loading$ is true until relatorioCobranca$ emits a result.

Upvotes: 1

Related Questions