This post is part of the ongoing Angular Architecture series, where we cover common design problems and solutions at the level of the View Layer and the Service layer. Here is the full series:
- View Layer Architecture - Smart Components vs Presentational Components
- View Layer Architecture - Container vs Presentational Components Common Pitfalls
- Service Layer Architecture - How to build Angular apps using Observable Data Services
- Service Layer Architecture - Redux and Ngrx Store - When to Use a Store And Why?
- Service Layer Architecture - Ngrx Store - An Architecture Guide
In this post we are going to see how an Angular application can be built around the concept of observable data services. This is one of several strategies available for building the application service layer. Let's go over the following topics:
- Alternative architectures for building Angular apps
- What is an observable data service
- how to use it
- RxJs Subject and how to use it
- BehaviourSubject and how to use it
- How to build an Observable Data Service
- Pitfalls to avoid
If you would like to see a very simple way to debug RxJs Observables, have a look at this post - How To Debug RxJs - A Simple Way For Debugging Rxjs Observables.
Alternative architectures for building Angular applications
There are several possibilities available for building Angular applications. There is this recent trend for building Flux-like apps with a single atom of state, in a style similar to Redux. The following are a couple of alternatives for building an app that way:
- if you are building an application in reactive style using the centralized store pattern, the recommended way is to use @ngrx/store guide here.
- If building the application using Redux itself, see this post for further details and a sample application
- If building the app using the concepts of Redux and the single state atom, but implementing it in Rxjs. See this other post for a way to do it and a sample app
This post will present an alternative that does not imply a single atom of state, and consists in using observable data services. If you are getting started with Observables and Angular, you might want to have a look at this post where we go over some common trouble scenarios.
What is an observable data service
An observable data service is an Angular injectable service that can be used to provide data to multiple parts of the application. The service, that can be named a store can be injected in any place where the data is needed:
In this case, we are injecting two services, one containing the application data which is a list of todos, and another service containing the current state of the UI: for example an error message currently displayed to the user.
How to use an observable data service
The data service exposes an observable, for example
TodoStore exposes the
todos observable. Each value of this observable is a new list of todos.
The data service can then be used directly in the templates using the
This pipe will subscribe to the
todos observable and retrieve its last value.
How to modify the data of a service
The data in services is modified by calling action methods on them, for example:
The data store will then emit a new value for its data depending on the action method call, and all subscribers will receive the new value and update accordingly.
A couple of interesting things about observable data services
Notice that the users of the
TodoStore don't know what triggered a new list of todos being emitted: and add todo, delete or toggle todo. The consumers of the store are only aware that a new value is available and the view will be adapted accordingly. This effectively decouples the multiple parts of the application, as the consumers of the data are not aware of the modifiers.
Notice also that the smart components of the application where the store is injected do not have any state variables, which is a good thing as these are a common source of programming errors.
Also of note is the fact that nowhere in the smart components is an Http backend service being directly used, only calls to the store are made to trigger a data modification.
Now that we have seen how to use an observable data service, let's see how we can build one using RxJs.
RxJs Subject and how to use it
The heart of an observable data service is the RxJs Subject. Subjects implement both the Observer and the Observable interfaces, meaning that we can use them to both emit values and register subscribers.
The subject is nothing more than a traditional event bus, but much more powerful as it provides all the RxJs functional operators with it. But at its heart, we simply use it to subscribe just like a regular observable:
But unlike a regular observable, Subject can also be used to emit values to its subscribers:
Subject has one particularity that prevents us from using it to build observable data services: if we subscribe to it we won't get the last value, we will have to wait until some part of the app calls
This poses a problem especially in bootstrapping situations, where the app is still initializing and not all subscribers have registered, for example not all
async pipes had the chance to register themselves because not all templates are yet initialized.
BehaviorSubject and how to use it
The solution for this is to use a BehaviorSubject. What this type of subject does it that it will return upon subscription the last value of the stream, or an initial state if no value was emitted yet:
There is another property of the
BehaviorSubject that is interesting: we can at any time retrieve the current value of the stream:
This makes the
BehaviorSubject the heart of the observable data service, we don't need much more to build one. Let's take a look at a concrete example.
How to build an Observable Data Service
You can find a full example of a store here, but this is the most important part of the service:
We can see that the store contains a single private member variable
_todos, which is simply a
BehaviorSubject with an initial state of an empty list of Todos.
The constructor gets injected with the Http backend service, and this is the only place in the application where this service is used, the remainder of the application has the
TodoStore injected instead.
The store gets initialized at construction time, so again it's important that we use a
BehaviorSubject otherwise this would not work.
But what is the reason behind that extra public member variable
Pitfall #1 - don't expose subjects directly
In this example we don't expose the subject directly to store clients, instead, we expose an observable.
This is to prevent the service clients from themselves emitting store values directly instead of calling action methods and therefore bypassing the store.
Avoiding event soup
Exposing the subject directly could lead to event soup applications, where events get chained together in a hard to reason way.
Direct access to an internal implementation detail of the subject is like returning internal references to internal data structures of an object: exposing the internal means yielding the control of the subject and allowing for third parties to emit values.
There might be valid use cases for this, but this is most likely almost never what is intended.
Writing an action method
In this type of application, the actions are simply methods made available by the stores. Lets for example see how the
addTodo action is built:
This is just one way to do it. We call the backend service which itself returns an observable either in success or in error.
We subscribe to that same observable, and on success we calculate the new list of todos by adding the new todo to the current list.
Pitfall #2 - avoid duplicate HTTP calls
One thing to bear in mind in this example is that the observable return by Http would have two subscribers: one inside the
addTodo method, and the subscriber calling
This would cause (due to the way that observables work by default) a duplicate HTTP call, because two separate processing chains are set up. See this post for further details on this and other ways that observables might surprise us.
To fix this issue, we could do for example the following, to ensure that no duplicate http calls can occur:
Beware of the tradeoffs of returning a shared observable instead of the plain HTTP observable directly:
now there are no duplicate network calls
but the callers of
saveTodomight not be able to do certain operations themselves (like retry).
For most CRUD-style data modification operations, this is actually a very good compromise.
For CRUD operations, we don't expect a new call to the backend to be made with every subscription to the Observable returned by the service layer.
Instead, we want to be able to build our view layer without worrying about duplicate data modification calls.
So for most CRUD backend operations, returning a shared observable from the service layer using
shareReplay() tends to work very well.
We avoid any accidental duplication of data modification HTTP requests that can happen to to multiple view layer subscriptions, and if by some reason we need access to the plain HTTP observable we can always do so in a separate method.
Observable data services (also known as store services) are a simple and intuitive pattern that allows tapping into the power of functional reactive programming in Angular without introducing too many new concepts.
Familiar concepts like the Subject which is basically an event bus are at the basis of this pattern, which makes it easier to learn than other patterns that need several other RxJs constructs.
Some precautions like not exposing the subject directly are likely sufficient to allow to keep the application simple to reason about, but this depends on the use case.
As we have seen in the pitfalls sections, some familiarity with RxJs and how observables work is required. Check this previous post for more details.
Want to Get Started With Angular?
If you want to learn more about Angular, have a look at the Angular for Beginners Course:
Other posts on Angular
If you enjoyed this post, here some other popular posts on our blog:
- Angular Router - How To Build a Navigation Menu with Bootstrap 4 and Nested Routes
- Angular Router - Extended Guided Tour, Avoid Common Pitfalls
- How to run Angular in Production Today
- How to build Angular apps using Observable Data Services - Pitfalls to avoid
- Introduction to Angular Forms - Template Driven, Model Driven or In-Between
- Angular ngFor - Learn all Features including trackBy, why is it not only for Arrays ?
- Angular Universal In Practice - How to build SEO Friendly Single Page Apps with Angular
- How does Angular Change Detection Really Work ?