There are two types of people in the world: those who go around dividing everything they see into two types, and those who don’t. As you can see, I tend to be one of those who does that.
So one way of dividing Observable
s into two types is: (a) single-use ones that are really glorified Promise
s, from which they can also be easily converted back and forth, and (b) long-lived ones that the subscriber has basically no clue when will stop emitting things.
An HttpClient
request is an (a). You don’t need to stress out about unsubscribing from (a) Observables. It is best to avoid keeping subscriptions to (b) Observable
s hanging around because they represent a resource leak. Dealing with that is a bit unwieldy, so various idioms sprang up to make it somewhat easier, and @UntilDestroy
is the smoothest I’ve seen, so I use it. It comes from ngneat/until-destroy.
We fundamentally have a tristate situation here:
- App has just started, nobody has asked the server for anything yet, we have no idea what products there are.
- For some reason (overly-aggressive search criteria, for example), the set of products that we are considering “all” is empty.
- We have some products.
3 is easy. Distinguishing between 1 and 2 is less so. One way of doing so is to reserve []
for state 2, and use null
or undefined
as a sentinel for state 1. Ordinarily when a service is providing arrays of things, it’s likely that consumers of it are going to try to iterate across it (using *ngFor
, for example). Feeding null
or undefined
to them in that situation is dangerous, because it’ll make Angular puke. There’s another way of doing this with ReplaySubject
s instead of BehaviorSubject
s, but a BehaviorSubject
needs an initial state. allProducts$
is allowed to hold a Product[] | undefined
for that reason. However, we don’t want anybody calling watchAllProducts
to know this, because it just burdens them with implementation details. So, that filter
effectively makes the return value from watchAllProducts
wait until it has something to say before it says anything. It won’t just say “I have nothing to say”. The double negation operator (!!
) is one of an admittedly bad bunch of alternatives for saying “be true
if my argument is truthy”, because JavaScript has very loosey-goosey notions of “truthiness” and “falsiness” that can get us into trouble.
The timing on when fetchAllProducts
should be called is outside the scope of our conversation so far. Maybe you want to do it at app startup, maybe you want to do it periodically, maybe there’s a separate product search page that makes more sense: I don’t know. One nice thing about this design is that you can do it whenever and wherever makes sense to you. You’re right: it has to be done at least sometime.
It’s true that the Observable
that you get from watchAllProducts
will emit the freshest set of data whenever anybody calls fetchAllProducts
. That’s sort of the point of all this.
There is a potentially problematic situation where subscribing to the same HTTP request in multiple places causes multiple HTTP requests. It happens because of hot and cold Observable
s, and can be mitigated in many ways - the share
operator is probably the simplest. However, it shouldn’t be biting you here. Avoiding that trap is why the Observable
coming back from the HTTP get
isn’t ever returned to the outside world. fetchAllProducts
doesn’t return what one would ordinarily think it would - it returns the same BehaviorSubject
as watchAllProducts
precisely so that there’s only one subscription to that request.
So by “calls” do you mean “HTTP requests over the network” or something else?