I was working with a piece of code recently that made liberal use of the “Tap” operator on an observable. I’ve seen it used pretty often, but everytime I try and find documentation to show a junior developer how it works, I always find it a really overcomplicated mess. Tap, infact, is a really simple operator on the surface that you really shouldn’t have a hard time understanding.
Let’s jump straight into some code. The most common use-case for using tap is because you want to use the result of an observable elsewhere, but still pass the exact same result back to any subscriber.
For instance let’s say that I want to call an API to fetch the current user, and I want to do nothing more than log that user to the console, and then return that same user out. A very naive way to achieve this would be :
this.http.get<User>('api/user').pipe(map((user : User) => {
console.log(`Current User Is : ${user.name}`)
return user;
}));
Theoretically not a heck of a lot wrong with this, I mean it works, but it’s not nice. You are calling the map function purely so that you can log the user, but then you aren’t actually mapping anything, and instead returning the same user. The result of this function is still Observable<User> so the caller doesn’t know what’s going on behind the scenes, but it’s messy.
One of the worst ways I’ve seen someone try and achieve this looked like so :
return Observable.create((observer: Observer<User>) => this.http.get<User>('api/user').subscribe((user : User) => {
console.log(`Current User Is : ${user.name}`)
observer.next(user)
observer.complete()
}));
I mean talk about RxJS word salad. It’s a mess. And we don’t need to do this if we just use Tap!
Taking the above example and using Tap.
this.http.get<User>('api/user').pipe(tap(user => {
console.log(`Current User Is : ${user.name}`)
}));
Notice how we used tap to write the console log, but we didn’t have to return the user object. It’s because we are saying that Tap will do something with the user object, but the original user should be returned to whoever is subscribing.
Another way to remember what tap does is that you are “tapping” into the result. Like a wiretap almost! You are listening in but (theoretically), you aren’t interfering with the existing conversation.
There is one caveat when using Tap though, and that is that the object inside the tap is still a reference to the original, it’s not a clone. So for example :
//This will return a user object with the firstName of test.
this.http.get<User>('api/user').pipe(tap(user => {
user.firstName = 'Test';
}));
I personally prefer to use Tap only when the method inside the tap is safe and has no side effects that would alter the original result. If I am going to be altering the original result, I prefer to use the Map operator because that signals that we are “mapping” what the original result was to something new (Even if it’s only changing a single property).
💬 Leave a comment