RxJS Mastery – #63 tap

The RxJS tap operator is often used or rather misused as an operator to log intermediate results in the Observable stream. But the tap operator can do a lot more than just logging. It is used to perform side effects without changing values.

const source$ = from([1, 2, 3]).pipe(
    map(v => v * 2),
    tap(console.log),
    map(v => v * 2),
);

expectObservable(source$).toBe(
    '(abc|)', { a: 4, b: 8, c: 12 }
);

In the above example, 2, 4, and 6 are logged to the console as intermediate results after the first map. Importantly, you should not use any other operator for side effects. Otherwise, those may become impure. Tap is specifically defined for that task and does not affect the notifications in the stream.

RxJS tap can also check on complete or error notifications

The example tap(console.log) takes next notifications into account. But let us have a look at the operator’s signature:

tap<T>(
    observerOrNext?: Partial<TapObserver<T>> | ((value: T) => void),   
    error?: (e: any) => void,
    complete?: () => void
): MonoTypeOperatorFunction<T>

We see that the tap Operator could also do side effects on the other two notifications, i.e. error and complete. It’s possible to pass an Observer, e.g. a Subject to tap. This can come in handy when you have an existing Subject and would like to connect it to the stream.

const subject$ = new Subject();
subject$.subscribe({
    next: (v) => console.log(`subject got the value: ${v}`),
    complete: () => console.log('subject saw the completion'),
});

const source$ = from([1, 2, 3]).pipe(
    map(v => v * 2),
    tap(subject$),
);

expectObservable(source$).toBe(
    '(abc|)', { a: 2, b: 4, c: 6 }
);

This will log:

subject got the value: 2
subject got the value: 4
subject got the value: 6
subject saw the completion

Attention in case of errors during tap

Sometimes the stream can still be affected. In the following example, there is an error during tap. This can indeed have an influence on the stream itself. An error is emitted:

const source$ = from([1, 2, 3]).pipe(
    map(v => v * 2),
    tap(v => {
        if (v === 6) {
            throw 'error in the side effect';
        }
    }),
    map(v => v * 2),
);

expectObservable(source$).toBe(
    '(ab#)',
    { a: 4, b: 8 },
    'error in the side effect'
);

Exercise for the RxJS tap operator 💪

Use tap to mutate the state on a isLoading variable. Once the mapping is done the isLoading can be set to false. Is this a good approach?

let isLoading = true;

cold('---r', { r: { json: '{ "content": "content" }' }}).pipe(
    map(r => r.json),

);

This post is part of the RxJS mastery series. As always the code examples can be found on GitHub.