简体   繁体   中英

Do parts of an rxjs pipe only for certain emissions of the Observable

I have an rxjs-Observable which I subscribe to. I now have two different necessities to handle within the pipe of the Observable.

First I want to map the output. Second I want to use tap to trigger a side-effect, but the side-effect must not be triggered on the first emission.

So this obviously does not work, because the skip is working globally on the pipe:

this.userChangeSubscription = this.userStateService.userState$
  .pipe(
    map(userState => userState.prop),
    skip(1),
    tap(() => this.sideEffect())
   )
  .subscribe();

Is there any way to do this, without subscribing to the Observable twice?

edit : Ok, I now have several options that all seem to be working. Now: which one to choose?

I've seen that there are already working answers, but I think your best bet would be to write your own rxjs operator for this usecase. This results in a clean solution and inside your pipe function clarifies your intent.

To do so we need to make use of thedefer observable :

function tapSkipFirst<T>(fn: Function): OperatorFunction<T, T> {
  return function(source: Observable<any>) {
    return defer(() => {
      let skip = true;
      return source.pipe(
        tap((v?:any) => {
          if (!skip) {
            fn(v);
          }
          skip = false;
        })
      );
    });
  };
}

We are using the skip variable to decide whether we run the side effect function or not. At the end of the first run we toggle skip to be false and therefore run the side effect function for all following runs. We need to use the defer observable here, because the code inside defer only gets called on subscription and not on creation. This is important because otherwise all subscriptions would share the same skip variable.

And now you can easily use the new custom operator like:

this.userChangeSubscription = this.userStateService.userState$
  .pipe(
    map(userState => userState.prop),
    tapSkipFirst(() => this.sideEffect())
   )
  .subscribe();

Yes it is possible. Example:

const userProp$ = this.userStateService.userState$.pipe(
  map(userState => userState.prop),
  shareReplay({ bufferSize: 1, refCount: true })
);

Here shareReplay will let you share that resource between multiple subscribers without triggering the whole chain again. Once all the subscribers are done listening, the shareReplay will end and the observable above will be ended.

Then you can subscribe as many times as you want and also isolate the side effect which is a good idea in the first place:

userProp$
  .pipe(
    skip(1),
    tap(() => this.sideEffect())
  )
  .subscribe();

You can use the switchMap operator for such use case. It gives you an index parameter allowing you to perform different actions based on the emission being the first, second, third etc..

In this case, the sideEffect will not get triggered if the index equals 0 (the first emission) but will get triggered for any other futures emissions.

I use this operator for triggering side effects or altering the value being emitted based on a condition. This is great because you don't need to split the source Observable into two different Observables having a different pipeline.

this.userChangeSubscription = this.userStateService.userState$
  .pipe(
    map(userState => userState.prop),
    switchMap((value, index) => {
      return index > 0
        ? of(value).pipe(tap(() => this.sideEffect()))
        : of(value);
    })
  )
.subscribe();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM