observable
observable copied to clipboard
Semantics of `do` (aka `tap` in many impls)
do
is a simple but important method for Observable. It allows for repeatable side effects, and is commonly used for logging etc.
An example use case would be if someone were to create some sort of shared connection observable and pass it around to users, such as a component that may be mounted more than once, but they want to log what's going through it.
/** connection.js */
// Underlying impl could be anything. Websockets, http/2 streaming, SSE.
const sourceStream = getSharedStreamData('someurl.com/whatever');
export const connection = sourceStream
// Use `do` to add logging to an observable
// we might subscribe to many times.
.do({
subscribe: () => {
console.info('stream connected');
},
next: (value) => {
console.debug(`stream next`, value);
},
error: (error) => {
console.error(error);
},
complete: () => {
console.info('stream complete: disconnected by source'),
},
abort: () => {
console.info('stream disconnected by user')
},
})
/* consumer-component.js */
import { connection } from './connection.js';
export ConsumerComponent extends HTMLElement {
constructor() {
this.controller = new AbortController();
}
connectedCallback() {
connection.subscribe(
(update) => this.handleStreamingUpdate(update),
{ signal: this.controller.signal }
);
}
disconnectedCallback() {
this.controller.abort();
}
handleStreamingUpdate(update) {
// TODO: update the view or something.
}
}
Other information
For a VERY LONG TIME, RxJS only mirrored what you could do in subscribe
in do
(aka tap
in RxJS). However, there were many valid requests to be able to use do
to inspect when you subscribe or unsubscribe. This isn't necessary at subscribe
because you know when you subscribe when you actually call subscribe
. Similarly the abort
(unsubscribe
in RxJS) callback is not required when the consumer aborts, because the consumer knows when it aborts, it just did it. It owns that code.
This method is also one of the better ways to do debug logging so you can see what is happening over time, and is in popular use there. I'm actually surprised that Promise doesn't have a do
. Without do
, most people will just use source.map(x => (/* side effect */, x))
, which is limited, not very semantic, and inefficient.
(fwiw i think tap
is a much more intuitive and understandable name than do
, given that do
is a language keyword)
@ljharb: Interesting. RxJS originally had do
, and I think I pulled tap
out of my butt (or someone else's) because do
was a keyword. I'm indifferent on the name, TBH. If everyone likes tap
, that's less education for folks using RxJS, I suppose.
Some previous discussion in https://github.com/WICG/observable/issues/29
(On the subject of naming, Rust spells this .inspect
.)
I also like tap
and think it got some good reception in #29 as @bakkot points out, and it's especially promising that it might have a place in iterator helpers some day, under that name, given https://github.com/WICG/observable/issues/29#issuecomment-1656430426. With that, I think I'll move forward spec'ing tap()
as proposed here.
I would maybe call it inspect
, following Rust? That seems like a more obvious name. But either's ok.
That might be even better, especially since there is some language precedent with it.