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. Have a look at angular2-redux-store for a sample. 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
- Conclusions
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
open
flag 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
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 Store
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.
Actions
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.
Dispatcher
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:
{ todos:[ { "id":1, "description":"TODO 1", "completed":false }, { "id":2, "description":"TODO 2", "completed":false } ], uiState: { actionOngoing: false, message: "Ready" }
We can see the state is separated into two parts:
- the application data, under property
todos
- the global Ui state, under property
uiState
Creating a Redux Action
Now let’s define some application actions. We do so by defining some Action Creator methods:
function addTodo(newTodo: Todo) { return { type: ADD_TODO, newTodo } } function toggleTodo(todo: Todo) { return { type: TOGGLE_TODO, todo } }
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.
Handling actions
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:
function todos(state: List, action) { switch(action.type) { case ADD_TODO: return state.push(action.newTodo); case TOGGLE_TODO: return toggleTodo(state, action); ... default: return 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:
function uiState(state: List, action) { switch(action.type) { case BACKEND_ACTION_STARTED: return { actionOngoing:true, message: action.message }; case BACKEND_ACTION_FINISHED: ... } }
We can combine reducers that act on different parts of the state, by using the combineReducers
Redux API:
const todoApp = combineReducers({ uiState, todos });
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:
import {createStore, applyMiddleware} from 'redux'; const createStoreWithMiddleware = applyMiddleware(createLogger())(createStore); const store = createStoreWithMiddleware( todoApp, { todos:List([]), uiState: initialUiState });
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:
import {ReduxStore} from "angular2-redux-store"; @Injectable() class TodoStore extends ReduxStore { constructor() { super(store); } }
See here for a complete example. The store can now be injected in any component, and used to dispatch actions:
class TodoList { constructor(private store: TodoStore) { } toggleTodo(todo) { this.store.dispatch(toggleTodo(todo)); } }
Immutable.js and immutable data collections
There are no real benefits point 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.
Other languages like Scala have built-in immutable collections that allow that, Javascript has a library that does something similar: Immutable.js
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:
let todo1 = { id:1, description: "TODO 1", completed: false }; let list = Immutable.List([todo1]);
This immutable list behaves like an array in the sense that it implements all the Javascript array API: push, filter, map, reduce, slice, etc.
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?
let todo2 = { id:2, description: "TODO 2", completed: true }; let list2 = list.push(todo2); // this list now has two elements
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 Objects
Immutable.js also foresees Immutable object-like structures that can be nested. Let’s create an immutable Todo object:
let todo = Immutable.Map({ id:1, description: "TODO 1", completed:false });
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.
The 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.
Immutable Records
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:
let TodoRecord = Immutable.Record({ id: 0, description: "", completed: false });
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:
let todo = new TodoRecord({id:1,description:'TODO 1'});
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:
console.log(todo.description);
This prints 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
As we know, in Javascript the inheritance mechanism is prototypical. The extends
keyword is really extending an object and not a class. So what happens if we create a TodoRecord object, and then extend it?
const TodoRecord = Record({ id: 0, description: "", completed: false }); class Todo extends TodoRecord { constructor(props) { super(props); } }
Instances of the Todo
class will have the properties of a TodoRecord:
- instances of this class are immutable
todo.description
and 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:
class Todo extends TodoRecord { id:number; description:string; completed: boolean; constructor(props) { super(props); } }
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.
Its not ideal, but its the closest we can get (AFIK) of a type-safe immutable class in Javascript (and here is the complete example).
We can now use the Todo class to build immutable Todo instances, and build a type-safe program around the Todo class:
let todo:Todo = new Todo({id: 1, description: "I'm Type Safe!"});
The todo
variable is strongy typed, immutable and its properties are accessible with the same convenience as object properties.
Let me know if you are aware of a better way of defining immutable classes in Javascript, please share in the comments bellow.
Conclusions
All the ecosystem is in place to build solid Flux apps in Angular 2. A state container is available (Redux) with solid foundation principles and a whole echosystem of plugins, excellent documentation and community momentum behind it.
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.
It’s still early to know what an Angular 2 app will look like, but the Flux Architecture is a proven choice in the React world and a very natural fit for Angular.
Reference: | Angular 2 Application Architecture – Building Flux Apps with Redux and Immutable.js from our WCG partner Aleksey Novik at the The JHades Blog blog. |