How to Build Angular 2 Apps Using Observable Data Services — Pitfalls to Avoid

In this post, we are going to see how an Angular 2 application can be built around the concept of observable data services. Read on to learn more.

By  · Tutorial
Save
80.9K Views
In this post, we are going to see how an Angular 2 application can be built around the concept of observable data services. An application is available in this repository as an example of how to build Angular 2 applications this way. Let's now go over the following topics:
  • Alternative architectures for building Angular 2 apps
  • What is an observable data service and 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
  • Conclusions

Alternative Architectures for Building Angular 2 Applications

There are several possibilities available for building Angular 2 applications. There is this recent trend for building Flux-like apps with a single atom of state, in a style similar to Redux. Here are two alternatives for building an app that way:

  • Building the application using Redux itself, see this post for further details and a sample application.
  • Building the app using the concepts of Redux and the single state atom, but implementing it in Rxjs. See this other post for more information and a sample app.

This post will present an alternative that does not imply a single atom of state and consists of using observable data services.

What is an Observable Data Service

An observable data service is an Angular 2 injectable service that can be used to provide data to multiple parts of the application. The service, usually named a store, can be injected anywhere the data is needed:

export class App {
    constructor(private todoStore: TodoStore, 
                private uiStateStore: UiStateStore) {
    }
}

In this case, we are injecting two stores, one containing the application data which is a list of todos, and another store 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 async pipe:

<ul id="todo-list">
    <li *ngFor="#todo of todoStore.todos | async" >
        ...
    </li>
</ul>

This pipe will subscribe to the todos observable and retrieve its last value.

How to Modify the Data of a Service

The data in stores is modified by calling action methods on them, for example:

onAddTodo(description) {
    this.todoStore.addTodo(newTodo)
        .subscribe(
            res => {},
            err => {
                this.uiStateStore.endBackendAction();
            }
        );
}

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: an 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 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.

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 subscriptors.

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 you would with a regular observable:

let subject = new Subject();
subject.subscribe(value => console.log('Received new subject value: '))

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. Instead, we will have to wait until some part of the app calls next().

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.

The solution for this is to use a BehaviorSubject. What this type of subject does is that it will return upon subscription the last value of the stream, or an initial state if no value was emitted yet:

let BehaviourSubject = new BehaviorSubject(initialState);

There is another property of the BehaviorSubject that is interesting: we can at any time retrieve the current value of the stream:

let currentValue = behaviorSubject.getValue(); 

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 store:

@Injectable()
export class TodoStore {
    _todos: BehaviorSubject> = new BehaviorSubject(List([]));

    constructor(private todoBackendService: TodoBackendService) {
        this.loadInitialData();
    }

    get todos() {
        return asObservable(this._todos);
    }
    ...
}

We can see that the store contains a single 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 getter method todos()?

In this example, we don't expose the subject directly to store clients, instead we return an observable.

This is to prevent store clients from 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. Let's see for a moment what would happen if the getter did not exist.

Direct access to an internal implementation detail like 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.

asObservable Part of RxJs Beta 2

In the sample application, we used a custom asObservable() method to convert the subject into a plain observable. This custom method will not be needed in future versions of RxJs, and we will be able to simply do:

subject.asObservable();

Writing an Action Method

In this type of application, the actions are simply methods made available by the stores. For example, let's see how the addTodo action is built:

addTodo(newTodo:Todo):Observable {
    let obs = this.todoBackendService.saveTodo(newTodo);

    obs.subscribe(
            res => {
                this._todos.next(this._todos.getValue().push(newTodo));
            });

    return obs;
}

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 addTodo.

This would cause a duplicate Http call due to the way that observables work by default, 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, in this case the share operator was called on the backend service layer, to ensure no duplicate HTTP calls occur:

saveTodo(newTodo: Todo) : Observable> {
    var headers = new Headers();
    headers.append('Content-Type', 'application/json; charset=utf-8');

    return this.http.post('/todo', JSON.stringify(newTodo.toJS()),{headers}).share();
}

Notice the share() call in the end. If anyone has a better way to solve this, please let me know in the comments. By the look of it, it all works as designed.

Conclusions

Observable data services or stores are a simple and intuitive patterns that allow one to tap into the power of functional reactive programming in Angular 2 without introducing too many new concepts.

Familiar concepts like the Subject, which is basically an event bus, are at the basis of this pattern, making 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 one to keep the application simple to figure out, 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 and have a look at the sample app.

References

Managing State in Angular 2 Applications by Victor Savkin (@victorsavkin)

Managing state in Angular 2 using RxJs by Loïc Marcos Pacheco (@marcosloic)       

Published at DZone with permission of Vasco Cavalheiro, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.


Comments