skip to Main Content

in the link tap, it mentions

"Be careful! You can mutate objects as they pass through the tap operator's handlers."

What is the meaning of this?

I try the following test

obs3 = new Observable<string>((observer) => {
      console.log('Observable 3 starts')
      observer.next('1');
      observer.next('2');
      observer.next('3');          
      observer.next('4');
      observer.next('5');
      })

ngOnInit() {             
       this.obs3.pipe(tap(val=>val+1)).subscribe(
            val=>{console.log(val);}
        );
}

the console output is

Observable 3 starts
1
2
3
4
5

so The observable returned by tap is an exact mirror of the source, but then what is the meaning of the statement

Be careful! You can mutate objects as they pass through the tap operator’s handlers.

?

*************[Edit on 20230707] *******************************

respond with Ruth answer, I try to compare the input observable before tap and output observable after tap by ===, and they are not equals.

const inputObj = {
          a: 'test obj prop',
          b: 5,
          c: ['x', 'y', 'z']
        };
 of(inputObj) === of(inputObj)
        .pipe(
          tap((input) => {
            input.a = 'changed prop value';
            input.b = 60;
            input.c = ['1', '2', '3'];              
            //return input;
          })
        )? console.log('after tap:still equal'):console.log('after tap: Not equal');

the output is

after tap: Not equal

so this becomes another problem, it violates the API about

The observable returned by tap is an exact mirror of the source

3

Answers


  1. rxjs follows a functional approach. Which means that it encourages the use of "pure functions". "pure functions" should follow the immutability concept, i.e. should receive input, elaborate that input without changing it and return the output. They should not have "side effects", i.e. they should not change anything outside the scope of the function. These concepts should enhance readability, understandability and testability of sw.

    At the same time, programs without side effects are basically useless. So there must be a place, usually at the margin of pure function processing, where side effects take place.

    The tap operator is the operator designed to implement "side effects" in rxjs. In practical terms tap receives an input value, does whatever, and then returns that same input value received. If the input value received is an object, that object can be mutated. Hence the warning you read in the documentation.

    In you case you do not see any mutation since what you do is not "change the input" (which is a number and therefore can not be changed). The number you get in input is the same one tap returns.

    One more concept: Observers.

    What tap expects in input is an Observer. An Observer is an object of type

    interface Observer<T> {
      next: (value: T) => void
      error: (err: any) => void
      complete: () => void
    }
    

    which is exactly the same input expected by the function subscribe. Hence you can imagine that tap is a sort of subscription placed in the middle of a pipe of operators.

    The way you have used tap and subscribe passing to them just one function is a simplified way of using tap and subscribe and means that you are just passing the next function, ignoring error and complete.

    Login or Signup to reply.
  2. In addition to the excellent explanation by @Picci, I’d like to add why your input didn’t change.

    It’s because you passed numbers, not objects, as mentioned in the docs, to the tap. Numbers are primitives, and by definition primitives are immutable.

    However, when you pass an object, you could see how it could be mutated inside the tap:

    const { of } = rxjs;
    const { tap } = rxjs.operators;
    
    const inputObj = {
      a: 'test obj prop',
      b: 5,
      c: ['x', 'y', 'z']
    };
    
    console.log('Before `tap`:', inputObj);
    
    of(inputObj)
      .pipe(
        tap((input) => {
          input.a = 'changed prop value';
          input.b = 'number -> string change';
          input.c = [1, 2, 3];
          
          return input;
        })
      )
      .subscribe(() => 
        console.log('After `tap`:', inputObj)
      );
    .as-console-wrapper { max-height: 100% !important; top: 0; }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.8.1/rxjs.umd.min.js"></script>
    Login or Signup to reply.
  3. This is the same warning as for any other functions receiving objects as arguments as those are passed as references and not as values.

    We can design our own tap function which would be affected in the same way:

    const tap = fn => x => (fn(x), x);
    
    const obj = {foo: 'bar'};
    
    ( tap(x => console.log(x.foo = 42)) //<- mutate
      )(obj);
    
    console.log(obj); //<- mutated! not {foo:'bar'} anymore
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search