As an Angular consultant, the main problem I see new Angular developers have is how to use RxJS efficiently in Angular apps. In other words, how to use it to create a scalable and maintainable reactive architecture. What I see instead is an abuse of RxJS, which is a completely natural thing to do, when you don’t know how to use it efficiently. This blog post will shed some light on this and will show you why and how to refactor your Angular apps to use a reactive architecture.
The problem with imperative programming
By definition, imperative programming means to use statements to change a systems state. In this way of programming, you will be setting data properties and maintaining the state explicitly through commands. The problem with this, might not be obvious once the app is small but might be the biggest root of Angular bugs as the app grows.
You see, Angular apps run in the browser where events are happening asynchronously. That means we are time-sensitive. So if we do imperative programming, we are sensitive to race conditions as well as mismanaged state, just by the order of how we execute commands and set state, eg. the state might not be available at the time it is read. Also, because of the explicit execution of commands, the different actors need to know about each other to call each other, which is limiting scalability and adding complexity. Often in reactive architectures, you would use some central mediator like a Redux store – more on this later in this post.
You see here, the data is only available once the data is fetched through the HTTP requests. This requires the consumer of the service to have explicit knowledge about the timing of the state and thus the inner workings of the service, breaking incapsulation.
What we want instead, is a declarative way of saying “give me the data once available”. This takes us to reactive architecture.
What is a reactive architecture?
Reactive architecture is a declarative programming paradigm where the state in the system is managed through data streams and events as a contrast to explicitly managing state through commands as in imperative programming (eg. map to state property).
As we see above, the reactive architecture will pass data down as it is available and will propagate changes up the dependency tree, giving us a one-way dataflow.
Let’s see why this is beneficial…
Why have a reactive architecture?
Having this architecture setup provides some nice benefits as the application scales:
- We are only getting the data when available, so we are not sensitive to timing and the execution order of commands but can simply declare: “give me the data when available in the stream and use it here”
- We are not sensitive to race conditions (if we are using RxJS operators correctly), as we can declare how data should “flow” through the stream in a controlled manner
- We are encapsulating data services/facades as we don’t need to know about their internals to use them, eg. we can just subscribe to a stream
- We are treating the code the same regardless if it is asynchronous or not
The last point is key. We make for very scalable architectures, especially in the asynchronous web, when we are handling asynchronous events in a controlled and declarative manner, so eg. we can easily control events such as show spinner while HTTP request is ongoing and remove again when done.
Variations of reactive architecture
The most common reactive architecture in Angular apps is Redux with NgRx (or other state management libraries) to manage the state in a Redux/store pattern. Here we will be modifying the state through dispatched actions (events), which are then going to handle side effects in effects (if any) and finally update the store in an immutable way using a reducer. Likewise, we can read from the store using selectors, which will give us an RxJS stream to subscribe to specific store data.
For NgRx, the flow goes like this:
Action dispatches action:
This action triggers loading the todo list which is a side effect and is handled in an effect:
After the HTTP request is handled a new action is dispatched to update the state in a reducer:
And then the state can be selected in the view by creating selectors:
And use them like this:
And then finally display the data in the template (using the async
pipe):
Even though Redux is a common way to manage state in Angular apps, it is not needed for a reactive architecture. Another variation for simpler Angular apps, that don’t need a full state management library, is simply to have state services holding state in private BehaviorSubjects
while exposing selector properties and methods to update the state.
Another option is to look into some of the more lightweight state management libraries like Akita if you need more features and don’t want to create your own state management solution. All of the best practices with state management in Angular and choosing the solution for you is already covered in-depth in Angular Architect Accelerator if you want to learn more.
Common violations of the reactive architecture
I hope the previous sections, got you sold on the reactive architecture because I can not stress enough how many bugs are due to not having a reactive architecture. It is so apparent, that I can tell that some part of the codebase has bugs by just identifying some of these common violations to the reactive architecture.
You see, Angular ships with RxJS but a lot of new Angular developers think:
“What the hell is this shit”, calls a subscribe
/tap
/toPromise
and map to a state property (sometimes even in the same component altogether – go big or go home).
The most common violations to reactive architecture are as follows:
- Map to state property using
subscribe
ortap
- Convert to promise with
toPromise
Let’s see how we can fix this.
Refactoring common violations to reactive architecture
Let’s go through how to refactor these common problems in reactive applications.
Refactoring: Map to state property using subscribe
or tap
A common problem is mapping to a state property to access the state. This makes no sense when we have the reactive superpowers of RxJS at our disposal.
Now, I know this is often a symptom of the developer not knowing the basic RxJS operators to efficiently work with data in a stream and in fact you don’t need to know all the RxJS operators, you just need to know the most commonly used ones. Read my blog post about the trivial and most used operators and that should get you going.
After learning the basics of RxJS operators, we instead want to keep the value in the stream until it is used. That means we keep it in the stream until the “end destination” is reached.
Data flow to one of two destinations:
- The templates
- As parameters to function/HTTP calls
Handling data flowing to the template
When data flows to the template, which should be the most common scenario, we can simply use the async
pipe. This has the benefit of less code, not needing to subscribe manually and will automatically unsubscribe once the component is destroyed.
As parameters to function/HTTP calls
Now, in many apps, they would be using NgRx with effects and here there is already a subscribed stream we can apply operators on. In this case, the most common scenario would be to do a withLatestFrom
in the effect like this:
This will give us the latest emitted value from the selector giving us the user’s role, we can use in the effect.
We want to avoid manually calling subscribe as much as possible but there are still cases, where it makes sense to manually subscribe. Common scenarios are subscribing to a FormControl’s valueChanges
or router events. But in all other cases, we want to just follow the two cases above.
Refactoring: Convert to promise with toPromise
As a basic rule of thumb, you should not mix promises and observable. Of course, you are sometimes using libraries that are returning promises but I recommend converting the return value to observables using the from
command so we are only dealing with observable streams in the Angular app for easier maintenance and consistency. There is also a difference between observables and promises: promises are only emitting one value and are always async and observables can be both sync and async. For a more in-depth explanation read here.
Conclusion
We saw the benefits of having a reactive architecture over an imperative architecture. We saw different ways to implement a reactive architecture and how to overcome common violations by refactoring to a reactive architecture.
Do you want to become an Angular architect? Check out Angular Architect Accelerator.