Tackling State - Angular 2

July 19, 2016 0 Comments redux, angular 2

Tackling State - Angular 2

Example

Since this pattern is similar to Redux, you may benefit from watching Dan Abramov's excellent video course on the subject. In this course Dan shows how to build a todo application using Redux. To make it easier for you to compare the Redux implementation and my implementation, I will build the same app in this article.

Application State

I get a really good feel of what an application does by looking at its state's type definitions and its list of actions. So let's start with that.

Our application's state is just a array of todos and a filter defining which todos to display.

Actions

Our application will support the following three actions: AddTodoAction, ToggleTodoAction, and SetVisibilityFilter.

The type Action, which is a union of the three actions, represents everything this application will be able to do.

Observable

We need to define the state function that will return an observable of application states. To make the example a little bit more production-like, I will split the state function into two parts: one dealing with todos and the other one dealing with the visibility filter.

Let's deal with todos first.

There are a few interesting things that I'd like to point out.

First, look at the signature of the function. The function does not take a single action, but rather an RxJS observable of actions. Likewise, it does not return a list of todos, but an observable.

Second, actions.scan applies the accumulator function over an observable sequence and returns each intermediate result. So a new list of todos will be emitted after every action.

Third, TypeScript realizes that the action inside the if clause is an AddTodoAction. This means that I can access the todoId and text properties, but not filter. This allows me to write such functions in a type-safe way. This is fantastic because such functions are where the smarts of your application live, and as a result, they quickly become non-trivial. So having some compiler support there is a big plus. If you are a Redux user and you are used to

{type: 'toggle', id: 15}

instead of

new ToggleTodoAction(15)

you are in luck. TypeScript 2.0 supports discriminated union types, so you can safely type your Redux-like code.

Next, let's extend todos by adding an ability to toggle a todo.

Similar to todos, we can implement a function creating an observable of visibility filter.

And, finally, we can combine them to create stateFn.

There is a lot going on in this six lines of code.

First, we create the todos and filter observables using the functions defined above. Then, we zip them into an observable of pairs, which we map into an observable of AppState.

There is one problem with this observable. If a component subscribes to it, the component won't receive any data until the observable emits a new event. For this to happen a new action has to be emitted. This is not what we want. What we want is for the component to receive the latest snapshot the moment it subscribes. And that is what BehaviorSubject is for.

A behavior subject is an observable that will emit the latest value to every new subscriber.

To better understand how stateFn works, let's write a few unit tests:


If you are familiar with Redux, you can find the stateFn function to be similar to a Redux reducer. But there is actually a big difference: the stateFn function is invoked only once, whereas a Redux reducer is invoked on every action.

This is important for the following reasons:


Just an Observable

The stateFn function is called only once to create the state observable. The rest of the application (e.g., Angular 2 components) do not have to know that stateFn even exists. All they care about is the observable. This gives us a lot of flexibility in how we can implement the function. In this example, we did it in a Redux-like way. But we can change it without affecting anything else in the application. Also, since Angular 2 already ships with RxJS, we did not have to bring in any new libraries.


Synchronous and Asynchronous

In this example, stateFn is synchronous. But since observables are push-based, we can introduce asynchronicity without changing the public API of the function. So we can make some action handlers synchronous and some asynchronous without affecting any components. This gets important with the growth of the application, when more and more actions have to be performed in a web-worker or server. One downside of using push-based collections is that they can be awkward to use, but Angular 2 provides a primitive - the async pipe - that helps with that.


Power of RxJS

RxJS comes with a lot of powerful combinators, which enables implementing complex interactions in a simple and declarative way. For instance, when action A gets emitted, the application should wait for action B and then emit a new state. If however, the action B does not get emitted in five seconds, the application should emit an error state. You can implement this in just a few lines of code using RxJS.


This is key. The complexity of application state management comes from having to coordinate such interactions. A powerful tool like RxJS, which takes care of a lot of coordination logic, can dramatically decrease the complexity of the state management.

Application and View Boundary

At this point we have not written any Angular-specific code yet, we have not written a single component. This is one of the benefits of this architecture - the application and the view logic are separated. But how do they communicate?

They communicate via the dispatcher and state objects.

We can create them as follows:

  • dispatcher is a RxJS subject, which means that it is both an observable and an observer. So we can pass it into stateFn, and use it to emit actions.
  • state is an observable returned by the stateFn function.

We can register the providers like this:

And then inject them into components.

No Store

Note, that in opposite to Redux or Flux, there is no store. The dispatcher is just an RxJS observer, and state is just an observable. This means that we can use the built-in RxJS combinators to change the behavior of these objects, provide mocks, etc.


No Global Objects

Because we use dependency injection to inject the state and the dispatcher, and those are two separate objects, we can easily decorate them. For instance, we can override the dispatcher provider in a component subtree to log all the emitted actions from that subtree only. Or we can wrap the dispatcher to automatically scope all actions, which can be very handy when multiple teams are working on the same application. We can also decorate the state provider to, for instance, enable debouncing.

View

Finally, we got to the most interesting part - implementing the view layer.

Displaying Todos

Let's start with a component rendering a single todo.

This is what Dan Abramov calls a dumb or presentational component. This component is not aware of the application side of things. It only knows how to render a todo.

Next, the todo list component.

The first thing we do here is we create an observable of filtered todos using the injected state. Since observables are push-based, they can be awkward to work with. We in the Angular team recognize this, and, that is why, provide a few things to make it easier. For instance, the async pipe "extracts" the last value of an observable. This allows us to use an observable of an object in any place where that object is required.

Second, we use the injected dispatcher to a emit new action in the emitToggle event handler.

This component is aware of the application because it injects the dispatcher and the state. Some can say this component mixes presentational and non-presentational concerns. So if you feel strong about it, you can separate this component into two. But I am not sure if it will buy you that much. Angular components already separate presentational aspects into a template. Splitting every such component into two might be an overkill.

Adding Todos

Next, let's create a component adding todos.


Filtering Todos

Now, let's add an ability to filter todos:


Root Component

Finally, we add a root component combining different parts into our application.


Tackling State - Angular 2


Tag cloud