Tackling State
Victor Savkin is a co-founder of nrwl.io, providing Angular consulting to enterprise teams. He was previously on the Angular core team at Google, and built the dependency injection, change detection, forms, and router modules.
Managing application state is a hard problem. You need to coordinate between multiple backends, web workers, and UI components. Patterns like Redux and Flux are designed to address this problem by making this coordination more explicit. In this article, I will show how we can implement a similar pattern in just a few lines of code using RxJS. Then I will show how we can use this pattern to implement a simple Angular 2 application.
Core Properties
When talking about an architecture pattern, I like to start with describing its core properties. Something that you can write down on the back of a napkin. The devil, of course, is in the details, and we will get to that. But a high-level overview is useful nevertheless.
In many ways what we are going to build is akin to Redux.
Immutable State
The whole application state is stored as an immutable data structure. So every time a change happens, a new instance of the data-structure is constructed. Even though this seems limiting at first, this constraint results in many great properties. One of which is that it can make your Angular applications quite a bit faster.
Interaction = Action
The only way to update the application state is to emit an action. As a result, most user interactions emit actions. And if you want to simulate an interaction, you just need to emit the right sequence of actions.
Application is fn(a:Observable):Observable
The logic of the application is expressed as a function mapping an observable of actions into an observable of application states.
function stateFn(
initState: AppState,
actions: Observable<action>): Observable<appstate> { ⦠}
This function is invoked only once. This is different from Redux, where the reducing function is invoked on every action.
Application and View Boundary
The application and view logic are completely separated. The dispatcher and state objects are the boundary through which the application and the view communicate. The view emits actions using the dispatcher and listens to changes in the state.
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.
It is fast!
The pattern described does not just make Angular applications easier to organize and refactor. It also makes it more performant. This is because when the application state is stored as an immutable data structure, and the changes in the state are represented as an observable, we can set the OnPush strategy for all the Angular components. To learn more about it, read Angular, Immutability, and Ecapsulation.
Other ways to manage state
Of course, this is not the only way to manage application state in Angular apps. For instance, you can use @ngrx/store. You can also write applications that do not use immutable data or observables, and instead use use-case services or DCI.
Summary
Coordinating between multiple backends, web workers, and UI components is what makes managing application state such a challenging task. Patterns like Redux and Flux help address this. In this article, I showed how easy it is to implement a similar pattern in just a few lines of code using RxJS. Then I showed how we can use it to implement a simple Angular 2 application.
Victor Savkin is a co-founder of Nrwl. We help companies develop like Google since 2016. We provide consulting, engineering and tools.
If you liked this, click the ð below so other people will see this here on Medium. Follow @victorsavkin to read more about monorepos, Nx, Angular, and React.