Angular 2 Application Architecture - Building Flux Apps with Redux and Immutable.js
In this post we will explore how to design an Angular 2 application according to the Flux architecture, by using the Redux and Immutable.js libraries. We will go over the following topics:
- The challenges of building large scale single page apps
- Three types of state in an app
- The Flux architecture
- The Redux state container
- The advantages of immutable state
- Immutable.js and immutable data collections
- Building a Flux Angular 2 app step by step
- How to use Immutable.js and still keep type-safety
Why is it so hard to build large single page apps
One of the biggest design changes that happened since we started building single page apps is that the state of the app is now completely on the client instead of the server.
The application state resides in the browser and is easily accessible for modification by any part of the app. The problem is that once the code gets larger and the team increases, this easy access to the application state starts to cause issues.
The danger of unconstrained mutable state
Allowing the state to be mutated from anywhere in the app at any time quickly gets out of hand. Bugs and subtle race conditions start creeping in, and its hard to refactor without unexpected side effects.
Keeping the frontend code maintainable is is all about:
- keeping the application state under control, so that the application remains simple to reason about
- ensuring the type-safety of the code, which allows the application to be refactored and maintained over time
The advantages of immutable state
If mutable state is dangerous, the safest alternative is to make our data read-only by default, or immutable. If the state needs to be changed we replace it by a new state in a controlled manner, but don't mutate the existing state. This has two major advantages:
- it makes it easier to reason about the application, because we know that the state can only be changed in certain places
- Frameworks like React or Angular 2 can take advantage of knowing that the state is immutable to optimize their change detection mechanisms and improve performance
Applications have three types of state
When we mention the application state, we need to make a distinction between several types of state:
- Internal Component State: each component of the component tree (like for example a dropdown) is bound to have some internal state, for example a
openflag to indicate that the dropdown is opened
- Global UI State: This state defines the way the user configured the UI: which language is active, which charts are visible, etc.
- Application Data State: This state is the data of the application, for example a list of countries passed to a dropdown
Different strategies might be needed for controlling different types of state. Let's see how the Flux Architecture can help with that.
The Flux Architecture
Flux is an architecture for building frontends that originally came from the React community. The main idea of Flux is that the state of the application can be controlled by keeping it inside a data store.
Let's go through the 4 main concepts of Flux:
The View is simply your Angular 2 component tree, meaning all the widgets that make up your app. In Flux the view is a tree of pure components that each act as a pure function: data comes in and gets rendered, but not modified directly by the View itself.
The fundamental notion of Flux is the Store, which is the container for the application state. The state exists inside the Store and cannot be modified directly by the View.
In Flux several stores can exist per application, for example one store for the contents of a data grid and one store for form data. But to make things simple let's consider that there is only one store.
it is essential that the data coming out of the store is immutable.
If the data inside the store is immutable, then how can it be modified? In Flux the only way to modify data is to dispatch an Action, that will trigger the creation of a new state. So the state never gets mutated, but a new version of it is created and used in place of the previous state.
An action is just a simple message object, that reports something that has happened: data loaded, Todo added, list sorted, etc.
The action also contains all information needed to create the new state of the store, such as: the new Todo added, the new sort order, etc.
The Dispatcher reports actions to any stores interested in receiving it. Several parts of the app (and so several stores) could act on a given action.
For example in an email app the folder list might want to update a counter if an email is received, but the main panel might want to display the new email subject in a list.
As our app only has one store, we don't need a dispatcher. Actions are dispatched directly against the store
Building an Angular 2 App using Flux
Let's build a simple app (available on this repository), that looks like this:
Notice that the app state is logged in the console. This is a functionality of the first library we are going to introduce to build the app: Redux.
The Redux State Container
Redux is a state container for building Flux apps. It follows a particular interpretation of Flux where the application only has a single store, and so no dispatchers are needed.
But the ability for different parts of the application to react differently to an action is still kept in Redux, as we will see.
The execution of actions can be wrapped in pluggable middleware such as redux-logger, which is used to produce the logging on the screenshot above.
Designing the application state
When creating a Flux app, its a good idea to start by defining the contents of the application state. For example the Todo App has the following state:
We can see the state is separated into two parts:
- the application data, under property
- the global Ui state, under property
Creating a Redux Action
Now let's define some application actions. We do so by defining some Action Creator methods:
These methods are used only to create an Action, not to dispatch it. As we can see the Action is only a simple POJO with a type string property that identifies the type of action. The action object contains any information necessary to carry out the action, like the new Todo.
But how does the state get changed according to the action? To define that, we write pure functions called reducer functions, with a signature identical to the reduce functional programming operator:
(state, action) => state
Given an initial state and an action, the reducer function returns the next state. Reducers are pure functions with no side effects and don't mutate the input arguments. They are simple to test and to understand.
Creating a Redux reducer
For example this is the reducer for the application data state:
The reducer just branches the calculation of the new state and delegates it to other functions, or calculates the new state directly in the switch statement if that only takes a couple of lines.
We also need a reducer for the global Ui state:
We can combine reducers that act on different parts of the state, by using the
combineReducers Redux API:
This creates one reducer that delegates the processing of part of the state to two smaller reducers.
Creating a Redux Store
With the reducers defined, we can now create a store. Most of the time you will want to add some middleware to the store, for example a logger:
Here we pass in the
todoApp combined reducer, and the initial state. The logger middleware is added as well, so the Redux store is now ready to use. But how to we use it in an Angular application?
Integrating Redux with Angular
The store will be needed in multiple places of the application. Any component that needs to dispatch an action will need access to the store.
Let's then make the store available anywhere via dependency injection. Check the Angular 2 Redux Store for a minimalistic approach on how to do that. We just need to take the store we just created and create a class like this:
See here for a complete example. The store can now be injected in any component, and used to dispatch actions:
Immutable.js and immutable data collections
There are no real benefits for storing state inside a store using Redux if we cannot make the state immutable in a practical way.
This means that we need to find a convenient way to both make the state immutable and to be able to create new versions of the state from the previous version without a lot of boilerplate.
This library is the missing piece needed for being able to build Flux applications in Angular 2. Let's see how it works.
Building immutable lists
To see how immutable collections work, let's start by creating an immutable list of one element:
The list can be iterated over using
for ... of in an Angular template, as it implements the Iterable interface.
The API is at the same time very familiar but quite different, because none of the familiar methods mutate the list. Not even push! What happens if we call push trying to insert a new element in the list?
The return value of push is a new list containing the new Todo, but the original list remains unmodified. Internally Immutable.js has efficiently created a new collection based on the first collection, without deep copying the data. This is done in a completely transparent way, and makes it simple to handle immutable collections.
The API of Immutable.js allows to easily obtain modified versions of deeply nested structures, which is exactly what we need to implement our reducer functions.
Immutable.js makes immutability practical, because it makes it easy to obtain new version of the immutable data based on the current version.
Immutable.js also foresees Immutable object-like structures that can be nested. Let's create an immutable Todo object:
This creates an object-like immutable structure, and the properties can be accessed like this:
let description = todo.get('description');
This syntax is not ideal, it would be ideal to be able to access the description using
todo.description. More on this later.
todo object itself is immutable, but it exposes a
set() method. If we call it we get back a new Map with the setted property modified accordingly:
let newTodo = todo.set('description', 'NEW TODO');
The description of
newTodo will then be
NEW TODO. An immutable Map can take any property as key, just like a POJO. With Map we won't be able to write type-safe programs, as it's a very generic data structure.
The next best thing after an immutable Map is an immutable Record. This is just like a Map, but it has a predefined allowed set of keys:
This creates a new prototype for a particular type of immutable object with those 3 specific keys. A new object of Type TodoRecord can be created like this:
This creates a new Record where two of the keys values (id and description) where specified, while the completed property takes its default value of false. If we try to set a property other than the 3 known properties we get an error.
Another advantage towards a plain Map is that we can now access properties using the normal object notation:
TODO 1 as expected. This is already a good start, but its still not type safe. There is an allowed set of keys with known types, but the Typescript compiler does not know about this, only Immutable.js has this information. Let's see if its possible to improve this.
Using Immutable Records in a Type-safe way
extends keyword is really extending an object and not a class. So what happens if we create a TodoRecord object, and then extend it?
Instances of the
Todo class will have the properties of a TodoRecord:
- instances of this class are immutable
todo.descriptionand all the accessor of its properties work as expected
But this is still not type safe! There is no way for the Typescript compiler to know the properties of the Todo class. But there is a simple way, we just tell it:
What we did here is we added the properties to the class. This way the Typescript compiler can auto-complete these properties, type check them and IDEs can refactor them and find usages.
We can now use the Todo class to build immutable Todo instances, and build a type-safe program around the Todo class:
todo variable is strongy typed, immutable and its properties are accessible with the same convenience as object properties.
A whole ecosystem is in place for building solid Flux apps in Angular 2. A state container is available (Redux) with solid principles and plugins, great documentation and community momentum behind it.
But make sure you really need Flux
Like any other technology, its important to know which use cases fit well Flux. See here for further recommendations on when to use Flux, from Pete Hunt (@floydophone) one of the developers of React:
Clearer instructions on when to use or not use Flux
Altough Redux core itself is very intuitive, once we start adding some of the plugins like redux-thunk or redux-promise, the complexity starts to increase. Take a look at the real-world example in the Redux repository to have an idea what a real app will look like.
Besides Redux, an immutable collections library exists (Immutable.js from Facebook) that can be used in a practical way to perform deep transformations of large object trees, and still do it in a type safe way.
RxJs and Functional Reactive Programming
Angular 2 will come bundled with Rxjs, which is also a promising library to help structure our app. See this post Angular 2 Application Architecture - Building apps with RxJs and Functional Reactive Programming (vs Redux) for an example of how to build the same Angular 2 application as in the sample repository but using RxJs.
Have a look at this other post How to build Angular 2 apps using Observable Data Services - Pitfalls to avoid for another aproach on how to build applications.
It's still early to know what an Angular 2 app will look like, but using Flux is a viable option: its a proven architecture that is known to have been successfully used to build some very complex apps (like Facebook itself).
Want to Get Started With Angular 2 ?
If you enjoyed this article, we invite you to subscribe to the Angular Academy Newsletter (box bellow).
If you are getting started with Angular 2, we will be publishing more lessons via this YouTube course which already includes guided tours to components and properties, as well as how to debug Angular 2 applications:
These two articles go over the use of Flux in Angular:
These two tutorials are a great way to learn Redux and Immutable.js: