RxJS Best Practices
RxJS is the most popular framework for reactive functional programming in
JavaScript. This means that a lot of people are using RxJS daily in their
projects. Most developers are aware of the common clean code practices, but…
What about RxJS best practices? Are you aware of the dos and don’ts when it
comes to functional reactive programming? Are you applying them in your code?
This tutorial will focus on several best practices that I use daily when writing
code, with practical examples. We will be covering the following points:
Avoid logic inside the subscribe function
Using Subjects to force completion
Avoid duplicated logic
Avoid nesting — Use chaining instead
Sharing to avoid stream duplication
Don’t expose Subjects
Use marble diagrams for testing
Without further ado, let’s get started:
Avoid logic inside the subscribe function
This statement may seem pretty obvious to some of you, but it’s a common pitfall for RxJS beginners. Until you learn how to think reactively, you may be tempted to do something like this:
Our pokemon$ Observable emits Pokemon objects, and, in a very non-reactive way, we are subscribing to it in order to access these objects and perform some actions, like returning early if the Pokemon type is Water, making a call to a getStats() function, logging the stats that this function returns and finally saving the data to the Pokedex. All our logic is inside the subscribe function.
However, doesn’t this code look exactly like something we would see in the traditional imperative programming paradigm? Since RxJS is a functional reactive programming library, we have to say goodbye to our traditional way of thinking, and start thinking reactively (streams! Pure functions!).
So, how do we make our code reactive? By using the pipeable operators that RxJS provides us with:
Et voilá, our code has gone from imperative to reactive with a few simple changes. It even looks cleaner, doesn’t it?
Note: I am fully aware of the fact that a part of the logic (the
saveToPokedex() function) still remains in the subscribe. I find that by keeping the last part of the logic inside the subscribe makes it easier for me to read the code. You are all free to choose whether or not to keep the subscribe completely empty :)
The operators we have used are pretty straightforward: filter and map work exactly the same as the Array operators that they share name with, and tap is used to perrform side effects.
Wikipedia: An operation, function or expression is said to have a side effect if it modifies some state variable value outside its local environment.
Using Subjects to force completion
Memory leaks are a real danger when it comes to using Observables. Why? Because, once we subscribe to an Observable, it will keep emitting values indefinitely until one of the following two conditions are met:
- We manually unsubscribe from the Observable.
- It completes.
Seems simple enough, right? Let’s take a look at how to unsubscribe from an Observable:
As you can see in the above example, we have to store the subscription of our pokemon$ Observable in a variable, and then manually call unsubscribe on that stored subscription. Doesn’t seem too difficult so far…
But what happens if we have more Observables that we need to subscribe to?
As you can see, as we add more Observables to our code, we need to keep track of more and more subscriptions, and our code starts looking a bit crowded. Isn’t there a better way for us to tell our Observables to stop emitting values? Luckily for us, there is, and it’s very, very simple:
We can use a Subject, together with the takeUntil() operator, to force our Observables to complete. How? Here’s an example:
Let’s understand what’s going on above. We’ve created a stop$ Subject, and we’ve piped our three Observables with the takeUntil operator. This operator is used for an Observable to keep emitting values, until a notifier Observable emits. Which means that our three Observables will stop emitting values when the stop$ Subject emits.
So how do we make our stop$ Observable emit? By calling the next()
function on it, which is exactly what we are doing inside our
stopObservables() function. Therefore, whenever we call our
stopObservables() function, our stop$ Observable will emit and all our Observables will automatically complete. Sounds cool, doesn’t it?
No more having to store any subscriptions and call unsubscribe, no more messing around with arrays? All hail the takeUntil operator!
Avoid duplicated logic
We all know that duplicated code is a bad sign, and something that should be
avoided. (If you didn’t know that, I would recommend that you go and read
this,
and then come back.) You may be wondering which scenarios could lead to having duplicate RxJS logic. Let’s take a look at the following example:
As you can see, we have a number$ Observable, which emits every second. We subscribe twice to this Observable: Once to keep score with scan() and once to call the getPokemonByID() function every ten seconds. Seems quite straightforward, but…
Notice how we have duplicated the takeUntil() logic in both Observables? This should be avoided, as long as our code allows it. How? By attaching this logic to the source observable, like this:
Less code && no duplication === Cleaner code. Awesome!
Avoid nesting — Use chaining instead
Nested subscriptions should be avoided at all cost. They make our code complex, dirty, difficult to test and can cause some pretty nasty bugs. What is a nested subscription, you may ask? It’s when we subscribe to an Observable in the subscribe block of another Observable. Let’s take a look at the following code:
Doesn’t look very neat, does it? The above code is confusing, complex, and, should we need to call more functions that return Observables, we’re going to have to keep adding more and more subscriptions. This is starting to sound suspiciously like a ‘subscription hell’. So, what can we do to avoid nested subscriptions?
The answer is to use higher order mapping operators. Some of these operators are: switchMap, mergeMap etc.
To fix our example, we are going to make use of the switchMap operator. Why? Because switchMap unsubscribes from the previous Observable, and switches (easy to remember, right?) to the inner Observable, which, in our case, is the perfect solution. However, please note that depending on which behavior you need, you may need to use a different higher order mapping operator.
Just look at how lovely our code looks now.
Sharing to avoid stream duplication
Ever had your angular code make duplicate HTTP requests and wondered why? Read on and you will discover the reason behind this widespread bug:
Most Observables are cold. This means that their producer is created and activated when we subscribe to them. This might sound a bit confusing, but is simple enough to understand. With cold Observables, every time we subscribe to them, a new producer is created. So if we subscribe to a cold Observable five times, five producers will be created.
So, what is a producer exactly? It’s basically the source of our Observable’s values (for example, a DOM event, an HTTP request, an array etc.) What does this imply for us reactive programmers? Well, if we, for example, subscribe twice to an observable that makes an HTTP request, two HTTP requests will be made.
Sounds like trouble.
The following example (borrowing Angular’s HttpClient) would trigger two
different HTTP requests, because pokemon$ is a cold Observable, and we are subscribing to it twice:
As you can imagine, this behavior can lead to nasty bugs, so, how can we avoid it? Isn’t there a way to subscribe multiple times to an Observable without triggering duplicated logic as its source its created over and over again? Of course there is, allow me to introduce: The share() operator.
This operator is used to allow multiple subscriptions to an Observable, without recreating its source. In other words, it turns an Observable from cold, to hot. Let’s see how it’s used:
Yes, that is really all we need to do, and our problem is ‘magically solved’. By adding the share() operator, our previously cold pokemon$ Observable now behaves as if it were hot, and only one HTTP request will be made, even though we subscribe to it twice.
A word of caution: Since hot Observables don’t replicate the source, if we subscribe late to a stream, we won’t be able to access previously emitted values. The shareReplay() operator can be a solution for this.
Don’t expose Subjects
It’s a common practice to use services to contain Observables that we reuse in our application. It’s also common to have Subjects inside such services. A common mistake many developers make is exposing these Subjects directly to the ‘outside world’, by doing something like this:
Don’t do this. By exposing a Subject, we’re allowing anyone to push data into it, not to mention that we are completely breaking the encapsulation of our DataService class. Instead of exposing our Subject, we should expose our Subject’s data.
Isn’t it the same thing, you may be wondering? The answer is no. If we expose a Subject, we are making all of its methods available, including the next() function, which is used to make the Subject emit a new value. On the other hand, if we just expose its data, we won’t make our Subject’s methods available, just the values that it emits.
So, how can we expose our Subject’s data but not it’s methods? By using the asObservable() operator, which transforms a Subject into an Observable. Since Observables do not have the next() function, our Subject’s data will be safe from tampering:
We have four different things going on in the above code:
Both our pokemonLevel and stop$ Subjects are now private, and therefore not accessible from outside our DataService class.
We now have a pokemonLevel$ Observable, that has been created by calling the asObservable() operator on our pokemonLevel Subject. This way, we can access the pokemonLevel data from outside the class, while keeping the Subject safe from manipulation.
You may have noticed that, for the stop$ Subject, we did not create an Observable. This is because we don’t need to access stop$’s data from outside the class.
We now have two public methods, named increaseLevel() and stop(). The latter is simple enough to understand. It allows us to make the private stop$ Subject emit from outside the class, thus completing all Observables that have piped takeUntil(stop$).
increaseLevel() acts as a filter and only allows us to pass certain values to the pokemonLevel() Subject.
This way, no arbitrary data will be able to find its way to into our Subjects,which are nicely protected inside the class.
Note: Please bear in mind that Observables have complete() and error() methods, which can still be used to mess with the Subject.
Remember everyone, encapsulation is the key.
Use marble diagrams for testing
As we all (should) know, writing tests is as important as writing code itself. However, if the thought of writing RxJS tests sounds a bit daunting to you… Fear not, from RxJS 6+, the RxJS marble testing utils will make life very, very easy for us. Are you familiar with marble diagrams? If not, here’s an example:
Even if you’re a newbie to RxJS, you should more or less understand these
diagrams. They’re all over the place, are quite intuitive, and make it quite easy to understand how some of the more complicated RxJS operators work. The RxJS testing utils allows us to use these marble diagrams to write simple, intuitive and visual tests. All you have to do is import TestScheduler from the rxjs/testing module, and start writing tests!
Let’s take a look at how it’s done, by testing our number$ Observable:
Since deep diving into marble testing isn’t the goal of this tutorial, I will only briefly cover the key concepts that appear in the above code, so we have a basic understanding of what’s going on:
TestScheduler: Is used to virtualize time. It receives a callback, which can be called with helper objects (in our case, the cold() and expectObservable() helpers.)
Run(): Automatically calls flush() when the callback returns.
- : Each - represents 10ms of time.
Cold(): Creates a cold Observable whose subscription starts when the test begins. In our case, we are creating a cold Observable that will emit a value very 10ms, and complete.
|: Represents the completion of an Observable.
Therefore, our expectedMarbleDiagram, expects for a to be emitted at 20ms.
The expectedValues variable contains the expected values of every item that is emitted by our Observable. In our case, a is the only value that will be emitted, and equals 10.
ExpectObservable(): Schedules an assertion that will be executed when the testScheduler flushes. In our case, our assertion expects the number$ Observable to be like the expectedMarbleDiagram, with the values contained in the expectedValues variable.
You can find more information about helpers etc. in the official RxJS
docs.
Advantages of using RxJS marble testing utils:
- You avoid a lot of boilerplate code. (Jasmine-marbles users will be able to appreciate this.)
- It’s very easy and intuitive to use.
- It’s fun! Even if you aren’t a big fan of writing tests, I can guarantee you’ll enjoy marble testing.
Since I enjoy making all my code examples Pokemon-themed, I’ll throw in another spec, this time featuring a pokemon$ Observable test:
Conclusion
That’s all folks! Today we discussed a few of the RxJS best practices that I always take careful care to apply in my code. I hope that you found them useful, if you didn’t already know about them.
Do you know any more RxJS best practices? If you do, let me know in the comments below. This way we can all contribute to writing better and cleaner reactive code!
If you enjoyed this post, don’t forget to share with your friends/colleagues, and maybe give me a lil’ clap :) If you have any questions, don’t hesitate to ask, either in the comments, or by reaching out to me via Twitter. See you all in the next tutorial!
Top comments (6)
Was this a small mistake?
const result = const result = number$.pipe(filter(number => isMultipleOfTen(number)));
Awesome article, thank you :)
Good catch! Should be fixed now, thank you :D
@nyagarcia In your first example about not putting logic in subscribe, the before and after code examples are the same.
Thank you, just fixed it :)
No worries. Great post, I've definitely made some of those mistakes before :0).
I think I found another problem though.
In the first example of unsubscribing from observable. You are not unsubscribing from the pokemonLevel$ observable, but calling next on stop$. Which, I think should not even be in this example, as you show that as an alternative solution in the second example, right?
Hey Rich,
I'm not really sure what's going on, all the gists are mixed up D: Which is odd, since this is basically a copy paste from the original post which is posted on medium, and the gists are fine on there... I'll change them again and see if they stay in the correct order, thanks for letting me know again!