When I first started in Angular and saw the RxJS library, I was like: whut is this? After some crying days, reading RxJs docs, and youtube tutorials, I started to get my hands on it, understand it, and appreciate it…. or as I thought.
Introducing the Problem
In my free time, I have been working on a money-tracking application called spendmindful.com, where users can track their incomes and expenses and see a visual representation of their data.
One of the functionalities is to filter out historical data by the selected month. You select a month in a year, the historical data is loaded into the table, an expense chart is calculated, and a tag aggregation is performed (i.e., how much money you once again spend in Starbucks). An example can be seen in the following illustration:
Loading data from the server is an async operation, so while we perform loading, it is a nice UX to show some sort of skeleton loader. Here is where the problem starts.
How was it Implemented
In the following snippet, you will see the initial implementation and, under it, a brief explanation.
// all daily data for a period
const totalDailyDataForTimePeriod$ = this.dateSource$.pipe(
tap(() => {
// displaying the skeleton loading
this.filteredDailyDataLoaded$.next(false);
}),
switchMap((dateFilter) =>
// get data from API
this.service.getPersonalAccountDailyData(dateFilter)
),
tap(() => {
// removing the skeleton loading
this.filteredDailyDataLoaded$.next(true);
}),
);
// creating another observable from totalDailyDataForTimePeriod$
this.accountFilteredState$ = totalDailyDataForTimePeriod$.pipe(
/* modifying data */
);
// creating another observable from totalDailyDataForTimePeriod$
this.filteredDailyData$ = combineLatest([
totalDailyDataForTimePeriod$,
this.selectedTagIds$
]).pipe(
/* modifying data */
);
To understand what is happening, here is the summary:
- Everything starts with
this.dateSource$
, which is just aFormControl
that emits a value every time the user switches to a different month. - When
this.dateSource$
emits, we set thethis.filteredDailyDataLoaded$
tofalse
, which will show that fancy skeleton loading on the UI. - The
switchMap
is a higher-order observable that takes an observable (the selected month) and returns a new observable (the API call to load daily datagetPersonalAccountDailyData()
) - Once the API call finishes, we set the
this.filteredDailyDataLoaded$
totrue
, which will remove the skeleton loading from the UI and show the components with the new data. - We save the 1-4 steps into a variable
totalDailyDataForTimePeriod$
because this variable is then used to create other observables (some data formatting) to which we already subscribe in the template (see the image below).
Where Did It Go Wrong?
At first glance, everything looks alright. Lots of RxJs, the implementation is very declarative, and we subscribe to observables only in the template with the async
operator, so what is wrong? Take a look at what the implementation resulted in:
The problem is that there is no loading screen even if the filteredDailyDataLoaded$
is set to false
. Moreover, instead of the loading screen, we almost have a blocking operation, where once the date is selected, nothing happens on the screen for ~2 seconds, and only when the data arrives from the BE, the UI is re-rendered.
I started doing some debugging around the loading screen, as you may guess by console.log
everything everywhere all at once ;).
The two main places I was the most curious about were when the date formControl
emitted a value and where the API returned the request. I was surprised to see the following outcome:
The inner observable, the totalDailyDataForTimePeriod$
logic was getting executed multiple times. Why was this happening?
To understand the problem, we have to talk about hot vs cold observables. In my logic, from the observable totalDailyDataForTimePeriod$
, additional observables are created to which I subscribe in the template with the async
pipe.
The totalDailyDataForTimePeriod$
is a cold observable, meaning every time a subscription is issued to this observable or to observables that are created from this one, a new instance of the totalDailyDataForTimePeriod$
in the memory is generated.
So we end up with multiple instances of the totalDailyDataForTimePeriod$
, all of them modifying the filteredDailyDataLoaded$
for showing/hiding the skeleton loaders, and all of the code is just a big race condition when the loader is displayed and when not.
What we want is to create a hot observable, which will compute totalDailyDataForTimePeriod$
body only once and share its most recent value with any new subscriptions.
Introducing the Fix
Making a cold observable to a hot one is, to be honest, it is quite straightforward. You have to use the RxJs share or shareReplay operator. I opted in for shareReplay
because it allows broadcasting the latest computed value for any new subscriber (something like if you have used behaviourSubject
). So the end fix was the following:
Summary
Angular with RxJs is one of the best combinations I have ever experienced as a frontend dev. It is a very declarative approach. An observable emits a new value, and every subscriber is notified. However, all of us make mistakes. This was one of mine, and I felt it is a good example to share to avoid it in the future.
Hope you liked this example. Feel free to follow me on eduardkrivanek.com, Github, Linkedin, or if you want to try out the application, go to spendmindful.com.
Top comments (3)
This hot/cold observable stuff is one of the trickiest bugs sources in Angular apps 🥲
For sure. The worst part is that you basically get no warning that the observable is triggering multiple times (each time per distinct subscription)... Despite of that I still like rxjs :D
Yea, the bloody Stockholm syndrome, I can relate 😅