Send a side effect http request after a successful one using rxjs operator

Viewed 930

My question headline may not be very clear, let me clarify more and you are welcome to improve it.

I have an angular application which polls a set of data every few seconds. Now I have a timer observable like this, and is subscribed in another place.

public readonly people$ = timer(3, 10000).pipe(
  switchMap(()=> this.http.get(url).pipe(tap(()=>{
      //here is some business logic
      //then at the end here I want to send a request unrelated to main business logic flow, e.g. auditing information update`
    })
  )
) 

I need this people$ observable to ONLY return people at the end, but meanwhile to trigger another http request to do something else that's unrelated to current stream.

What I have tried:

  1. Calling http.post(anotherUrl) inside the tap but obviously it won't work because it's not subscribed.
public readonly people$ = timer(3, 10000).pipe(
  switchMap(()=> this.http.get(url).pipe(
    tap(()=>{
      //here is some business logic
      this.post(anotherUrl); //won't trigger as no subscription and I don't think it's good to subscribe here
    })
  )
) 
  1. Changing tap to async and calling the request with toPromise, like:
public readonly people$ = timer(3, 10000).pipe(
  switchMap(()=> this.http.get(url).pipe(
    tap(async()=>{
      //here is some business logic
      await this.post(anotherUrl).toPromise();
    })
  )
) 

And this can work, but I wonder what should be a proper way to do this and whether I can achieve this in a rxjs way?

1 Answers

This can be simplified a bit:

public readonly people$ = timer(3, 10000).pipe(
  switchMap(_ => this.http.get(url).pipe(
    tap(_ => {
      //here is some business logic
    })
  )
)
//-------------
public readonly people$ = timer(3, 10000).pipe(
  switchMap(_ => this.http.get(url)),
  tap(_ => {
    //here is some business logic
  }),
);

Running an observable as a side effect of another stream is very likely a code smell. Doubly since you don't seem to want the result returned by the observable.

Either way, here are two ways to do it, though neither feels like idiomatic RxJS.

public readonly people$ = timer(3, 10000).pipe(
  switchMap(_ => this.http.get(url))
  tap(_ => {
    //here is some business logic
    this.post(anotherUrl).subscribe();
  })
);
//------------- Or this:
public readonly people$ = timer(3, 10000).pipe(
  switchMap(_ => this.http.get(url))
  tap(_ => {
    //here is some business logic
  }),
  mergeMap(people => this.post(anotherUrl).pipe(
    ignoreElements(),
    startWith(people)
  ))
);

How this should typically look:

You have three+ consumers (subscribers) of people$, so you'll want to multicast the source. There are many ways to do this, but share/shareReplay are two of the easier options.

Then in your constructor/initializer/wherever makes the most sense, you consume it separately for each side effect. You may also have a component in your UI displaying people, so it can be consumed there too.

Some things make sense in a tap operator; like logging/debugging, for example. Generally, it's probably bad (for maintenance and extendability) to start putting business logging inside a tap operator.

public readonly people$ = timer(3, 10000).pipe(
  switchMap(_ => this.http.get(url)),
  shareReplay(1)
);

public init(): void {

  people$.subscribe(_ => {
    //here is some business logic
  });

  people$.pipe(
    mergeMap(_ => this.post(anotherUrl))
  ).subscribe();

}

The benefit here is that you've created a separation of concerns rather than shoving side effects that are completely orthogonal inside a single observable. Makes this easier to update and maintain.

This is especially the case if/when you want to start catching and dealing with errors.

Related