0

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

user1169587
  • 1,104
  • 2
  • 17
  • 34

3 Answers3

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>
ruth
  • 29,535
  • 4
  • 30
  • 57
  • but if the object is changed, then how "The observable returned by tap is an exact mirror of the source"? – user1169587 Jul 06 '23 at 07:52
  • The observable is not the same as the value it emits. Infact, the uniqueness of an observable is it's ability to allow multiple values. From [docs](https://rxjs.dev/guide/observable#observables-as-generalizations-of-functions): "Observables are like functions with zero arguments, but generalize those to allow multiple values". So the object in our example was mutated, but the observable that emits the observable was not. In comparison, operators other than `tap` _may_ adjust the source observable and return a new adjusted observable. – ruth Jul 06 '23 at 12:24
  • sorry but I cannot catch the idea of" So the object in our example was mutated, but the observable that emits the observable (you mean the observable that emits the object?) was not. could you elaborate more about this statement? Or any article that can help to understand it? – user1169587 Jul 06 '23 at 16:43
  • @user1169587: I'm sorry that was a typo. It should've said "So the object in our example was mutated, but the observable that emits the _object_ was not." I hope this helps clear things up a bit. – ruth Jul 07 '23 at 06:27
  • but what is the object was mutated but the observable that emits the object was not? I try to compare the input observable and the output observable by === operator and it return they are not equal... – user1169587 Jul 07 '23 at 07:30
  • I'm not sure how you compared the observables. But using equality check `===` to compare observables let alone objects is quite useless. See [here](https://stackoverflow.com/a/12216624/6513921). – ruth Jul 07 '23 at 07:52
  • Let's come to the other question. You still aren't sure how `tap` is an _exact mirror_ of the source observable. Let's look at the sources. Here is [map](https://github.com/ReactiveX/rxjs/blob/72bc92191ab959e27a969dc4476e14d95416573f/src/internal/operators/map.ts#L48-L62) compared with [tap](https://github.com/ReactiveX/rxjs/blob/72bc92191ab959e27a969dc4476e14d95416573f/src/internal/operators/tap.ts#L167-L215). You could see the `tap` operator never adjusts the `value` variable that contains the emission from the source observable. However, `map` returns the return value from `project.call()`. – ruth Jul 07 '23 at 07:57
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.

Picci
  • 16,775
  • 13
  • 70
  • 113
0

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
customcommander
  • 17,580
  • 5
  • 58
  • 84