Anand Musaddi
Anand Musaddi

Reputation: 3

Issue with output of tap operator in RxJS

I am confused with the output of the below code, as per the take(3) processing should stop after rendering 10, but still getting 5,6 and 9 from tap operator. Please refer below output and code snippet.

of(null, 20, 15, 10, 5, 6, 9)
      .pipe(
        tap(val => console.log(`Tapped value ${val}`)),
        filterNil(),
        take(3)
      )
      .subscribe(
        item => console.log(`Rendering Item ${item}`),
        err => console.log(err),
        () => console.log('Completed')
      );
  }

const filterNil = () => (source: Observable<any>) =>
  new Observable(observer => {
    return source.subscribe({
      next(value) {
        if (value !== undefined && value !== null) {
          observer.next(value);
        }
      },
      error(error) {
        observer.error(error);
      },
      complete() {
        observer.complete();
      }
    });
  });

Upvotes: 0

Views: 1488

Answers (2)

Picci
Picci

Reputation: 17762

The final answer to this question

The correct answer to this question is provided by the following post.

My first answer - a simplistic answer

What you see depends on the fact that your code is completely synchronous, and therefore the unsubscribe after 3 emissions, which is implicit in take(3), does not have the chance to run.

Look at this version

of(null, 20, 15, 10, 5, 6, 9)
      .pipe(
        delay(0),  // >>> intruduce a delay
        tap(val => console.log(`Tapped value ${val}`)),
        filterNil(),
        take(3)
  )

Here you introduce a delay, which gives take the possibility to unsubscribe and, as a consequence, you see the behavior which you expect.

Not an answer but a more detailed reasoning

I have investigated a bit more into this problem and I have found some things which make my previous answer a bit too simple.

Let's start from the fact that filterNil() is a legitimate custom operator that should do the same as filter(item => item !== null), where filter is an operator provided by rxjs/operators, i.e. by the library.

Now, if we substitute filter(item => item !== null) to filterNil() in the pipe we obtain a different result

of(null, 20, 15, 10, 5, 6, 9)
      .pipe(
        tap(val => console.log(`Tapped value ${val}`)),
        filter(item => item !== null),
        take(3)
      )

      .subscribe(
        item => console.log(`Rendering Item ${item}`),
        err => console.log(err),
        () => console.log('Completed')
      );
  }

// the output on the console is

Tapped value null
Tapped value 20
Rendering Item 20
Tapped value 15
Rendering Item 15
Tapped value 10
Rendering Item 10
Completed

This means that filter(item => item !== null) and filterNil() are not equivalent.

The fact that they are not equivalent seems to come from the implementation of the subscribe method of Observable joint with the somehow different nature of filterNil and filter.

When using filterNil, the trace of the execution of subscribe method of Observable is this

enter image description here

If, on the other hand, we use filter operator, the execution trace of of subscribe method of Observable is this enter image description here

Therefore the fact that filterNil does have the operator attribute set to null while filter does have the operator attribute set to FilterOperator seems to drive the different behavior. The reasons behind are not clear to me and are worth a new question.

Upvotes: 1

probablykabari
probablykabari

Reputation: 1389

Your filterNil function is still running because that is a new observable that is not affected by the take operator. RxJS operators run in order. You can move the take to the initial position in the pipe, or I would recommend not creating a new observable in your filter function

// operator position matters
.pipe(
  take(3)
  tap(val => console.log(`Tapped value ${val}`)),
  filterNil()
)

// Or re-factored filterNil operator

const filterNilRefactor = () => {
  return source => source.pipe(filter((value) => value !== undefined && value !== null))
}

.pipe(
  tap(val => console.log(`Tapped value ${val}`)),
  filterNilRefactor(),
  take(3)
)

Upvotes: 0

Related Questions