I've been listening to the @AngularShow recently, and specifically to their great episodes about RxJS operators. So far two of promised four episodes have been released (one and two) and they have given me motivation to improve some especially complex bit of piping we have in our application.
The goal of this exercise was to reduce a group of subscriptions I had in my code to a single subscribe
. It guarantees that all decisions about the code's flow are made in a single place, or at least, as a single flow of decisions.
Background
Content Note: I reduced some of the complexity of the real project for the sake of this post, but the general ideas are kept intact.
The app loads data from two API endpoints. The first endpoint fetches the structure
of the app and the second endpoint fetches the specific values
to be placed over the structure. The structure, once loaded, never changes, but the values will keep going back and forth over the API. Because the structure is static, it is kept in the StructureService
memory while the dynamic values are kept in a store (ngxs, in case you're wondering).
Now here's where it starts getting interesting. There are a couple of pages in the module where the user can "land" and we always need to have the values and the structure available as soon as possible. So to keep thing DRY, we call both loading actions directly in the module's constructor.
I'll just say that I was really exited to find that I can make full use of dependency injection in the module's constructor. You can read more in Ben Nadel's post about this.
A world of Observables
We have two main observables:
-
structureComplete$
- avoid
observable that completes after structure data is stored in the service. -
valuesState$
- the values from the store.
The valuesState
from store
const valuesState$: Observable<Values> = this.store.select(ValuesState)
The store loads and fires its first values immediately with the default of null
, so we need to skip that and wait for some meaningful content from the endpoint. For that we're going to use the skipWhile
filter. I know that when the data loads successfully, the values
will never be null
, so that's what I check for.
const valuesState$: Observable<Values> = this.store
.select(ValuesState)
.pipe(skipWhile((values) => values !== null));
The structure
complete
The first page that is shown to the user will probably have the structureComplete$
observable still alive, so we'll concat
the structure with the values.
const valuesPendingStructure$ = concat(
this.structureService.structureComplete$ as Observable<unknown>, // to prevent the need of mapping
valuesState$
).pipe(skipWhile<Values>((v) => !v)); // skip the 'structureComplete'
I banged my head around this for a while, because I really don't need any data from the structureComplete$
observable. I just want to know it ... completed, and the information is safely stored in the service. All I really care about, once it's done, are the values
. I know the structure is in place, but I have to keep watching the values.
The Observable<unknown>
calms TypeScript's nerves and lets the valuesPendingStructure$
variable have the type of Observable<Values>
. The skipWhile<Values>((v) => !v)
enables us to skip the first value emitted, which comes from the structure completing.
Putting it all together
As I mentioned before, There are several pages which look at the structure observable, and while the first page to look at structureComplete$
will likely find it still loading, later pages will already see it complete and cannot subscribe to it. For that purpose I added a small isReady
getter to the structure service which defaults to false
, but sets to true
at the same time the observable completes and the structure is stored. To decide which of the values observables should I use, I added the iif
condition observable.
// Choose one of two options
// if structure is ready, get down to business
// if it isn't, wait for loadComplete$ and then hand over the values
const values$ = iif(() => this.structureService.isReady,
valuesState$,
valuesPendingStructure$)
);
The iif
operator can act both as an observable creator and as a pipe operator. In this case, we're using it as an observable creator, based on the value of isReady
. If the structure has already loaded, subscribe directly to the values
from the store. If we're still waiting for the structure to load, we'll use the observable that's waiting for it before switching to the values.
That's it. We can now subscribe to a single observable and always get the most up-to-date values
$values.pipe(takeUntil(this.destroy$)).subscribe({
next: values => {
// ...
},
error: error => {
// Inform the user something went wrong
// log the error for debugging/improving
}
});
Two important things to remember:
- We have lots of observables here, and we should always make sure to unsubscribe at some point. I really like the
destroy$
pattern which completes once when the page destroys and gives us a great reference point to unsubscribe. - All these observables, something is bound to go wrong. Make sure to always have an error handler on your subscription, so you can inform the user that something went wrong.
A small final note
As usual, while writing my posts, I had to break multiple time to continue working on this and other tasks. During some of those breaks I kept changing and improving the code, as well as discovering bugs and fixing them. I'm sure there are better ways to do this. For example, I think I can avoid having the structureComplete
observable and use the original HttpClient
observable.
If you have any additional thoughts on how this could be made better, I'd appreciate a comment!
Top comments (1)
great article on observables! i think they're often overlooked as one of the weird part of the angular stack, but has great potential to ease development pains if done correctly
with regards to your final comments regarding
structureComplete
- I've been experimenting with store service patterns using BehaviorSubjects and have pretty good impressions thus farlet me know if you ever pick it up and what you think :D