How to use the powers of reactive extensions in frontend
Anyone working on an Angular app should at least be familiar with RxJS. Actually the framework itself is build using RxJS and around some of its concepts. But there is more to that. In reality, we can use RxJS and observable streams to come up with better and more readable code and even reduce the number of lines (which translates to bundle size) that we write.
So let’s dive in!
Using RxJS to reduce the state of our components
Everything in Angular revolves around the state of the components (and the app in general) and how it gets projected to the UI. And there are lots of times when we can use streams to represent volatile pieces of our data inside the view. It especially comes in handy when working with forms and other stuff that changes a lot.
Here is how we do some stuff wrong:
- When we have a new task at hand, we think about how the state will change
- We store new pieces of state in our component (new properties, nested objects and etc)
- We devise new methods that encapsulate ways how our new state changes
- Write convoluted logic inside our template.
Let’s see this through an example:
Imagine we have a page which contains a
select box from which we can choose a user (the box is populated from an array using
*ngFor). Now there is also another select box with the same users, but now selecting some of them will make them blacklisted, so they are not allowed to be submitted, and if the user chooses one of those from the first
Submit button will become disabled. And there is another catch: there is also this “allow blacklisted users” checkbox, which, when selected, will allow to both blacklist someone and submit them (so the button won’t be disabled even if some of the selected users are blacklisted). Let’s try and implement it without using RxJS at all, with simple template-driven form models:
So we have two arrays, we have three form bindings, and another method that is being called on
(ngModelChange) to handle the change of our state. This is a classic example of the 4-step thinking I mentioned above, which makes us write more convoluted code.
- We think that we need some state (
allowBlackListedUsers) to store some data that is actually needed only in the template;
- We write that state properties on our component and bind them using
- We write a method (
changeUser) to handle that state change
- We also leave some logic in the template (
[disabled]=”isUserBlackListed && !allowBlackListedUsers”)
Why is this bad? For starters, following the logic of the application becomes harder. For example, if I am the one reading this code, and I see that a button sometimes becomes disabled, I will go through the following steps:
- Find the
[disabled]binding and see that it binds to two properties,
- Look them up in the component code and find that they are just some basic properties, starting from false;
- Then search the
component.tsfile to find references to them; This may seem like a no-brainer, but what if our properties are referenced in multiple methods? I would have to carefully examine them all to find out exactly which one affects the button being disabled;
- Read and understand the method I finally find. In our example, it is easy; in a real life component the logic may be far from trivial.
And another downside is that when another piece of such logic comes up, we will end up multiplying properties and methods that change them in our component code.
So what should we do?
More reactive thinking
Now we are going to devise a simple three-step plan of thinking. Trying to solve the same problem, but now using Reactive Forms and RxJS, we will do the following:
- Understand what part of the state affects the UI and make it an Observable stream;
- Use RxJS operators to perform calculations and derive the final state to be used in UI
- Use the
asyncpipe to put the result of our computations in the template
Here is the implementation:
As you may see, we implemented a property, which is an Observable, to handle some UI. It combines the output of our three form controls using
combineLatest, then uses their combined output to derive the boolean state.
Note: we used
startWithbecause in Angular
formControl.valueChangesdoes not start to emit until the user manually changes it through UI controls, or imperatively through
combineLatestdoes not fire until all of the source Observables emit at least once; so we make them all emit their default values once immediately.
Now , when I read this component’s template and think “when is this button disabled?”, I will go through the following steps:
- See the
[disabled]="isDisabled$ | async"binding; the dollar sign at the end will immediately betray that it is an Observable;
- Go to that property definition and see it is a combination of three sources of data;
- See how the data is mapped to a boolean
And that’s it. The
isDisabled$ Observable is not referenced in any other methods, and even if it was, it would not matter — others may subscribe to it, but they cannot change its data. If there is a bug and the button is disabled when it should not be (or vice-versa), then we can 100% be sure that the bug lies within the definition of
isDisabled$ and its operators and nowhere else.
So this change made our code:
- More easy to search and find something inside
- More concise; the pieces of logic that affect each other are collected in a single place and not scattered through the component
- Declarative instead of imperative
And all of that because of one simple concept:
Properties are easier to reason about than methods
So far so good. But what are other examples of how using RxJS may make our code in Angular better?
There are lots of examples of how two pieces of data are interdependent; one may change the other and the later may change the former. Here is a living example: imagine we have a component that has a search button; whenever we click on it, a search input appears right next to it; whenever we click somewhere else, it disappears, but not if something is written inside it already. Kind of like the search input Medium itself has — check it out, we are going to build one now.
Again, we are going to build it without RxJS first. Here is the implementation:
Here is what it does: we store a piece of state (boolean called
isSearchInputVisible), then we toggle it using two different methods, one on button click event, the other on overall document clicks. We also
stopPropagation on the button click for that click not to be confused with other clicks for other parts of the documents — it is supposed to open the search input, not close it! It is a naive implementation, but this is what one would go for when doing it without RxJS. And it incorporates in itself the 4 wrong steps I described in the first part of this article. OK, now let’s do it using RxJS:
Here we go: We have a single source of truth, no methods at all, and the entire logic comes from this operators on the source observables. Here is an explanation of what we have done:
- First we took two streams — the clicks on the button and the clicks on the entire document
- On the first stream — the button clicks — we first called
stopPropagation, and then mapped it to be
- The second stream — the clicks on the entire document — is being simply mapped to the value
false, but not when the
queryin not empty (hence the
mergeof this two streams is exactly what we want — the button opens the search input and the rest close it!
You may ask…
- Why did we put it inside
ngAfterViewInit? Because the
buttonRefwill not be available until the view is painted, so we have to wait until after that moment to be able to read events from it;
- Why did we need
.pipe(startsWith(false))after the merged
Observable? If we have not done it, the value of the
Observablewould have changed from
falsetoo fast, resulting in an
Didn’t we forget something?
You may think we need to unsubscribe from our
Observable, but in fact we don’t — the
async pipe does that for us.
And yet again converting our logic from imperative style to declarative using RxJS made our code way better.
RxJS is a powerful tool — no wonder such a huge enterprise framework like Angular is built around it; It has lots of concepts and tricks which may be used to make our code in Angular better, more readable, easier to reason about, more understandable. In no way is it limited to the examples in this article — in my future articles I will explore more use cases of RxJS in Angular.