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 Observables into two types is: (a) single-use ones that are really glorified Promises, 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) Observables 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 ReplaySubjects instead of BehaviorSubjects, 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 Observables, 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?