Angular 2 — Implementing FLUX architecture
A simple and elegant way to implement unidirectional data flow
Flux is an architecture for unidirectional data flow. By forcing the data to flow in a single direction, Flux makes it easy to reason “how data-changes will affect the application” depending on what actions have been issued. The components themselves may only update application-wide data by executing an action to avoid double maintenance nightmares.
Redux — usually seen with React but it can be used separately — proved to be a successful implementation of Flux. It can handle application state and bind it to the User Interface in a very effective way. RefluxJS, another independent implementation, refactored Flux to be a bit more dynamic and more Functional Reactive Programming (FRP) friendly.
Inspired by these implementations, I wrote a library StateX, that will help you implement a unidirectional data flow architecture for angular 2 (or above). In this article, we are going to see how to use this library to implement Flux architecture in 5 simple steps.
Install
npm install statex --save
5 Simple Steps
1. Define State
To get the best out of TypeScript, declare an interface that defines the structure of the application-state. Just like in Redux, application-state (or simply State
) is an object that will contain the entire data of your application. This step is optional; you could simply get away with any
, but I strongly recommend that you define an application state.
If you’ve used different flux alternatives in the past — with the state of your application spread across multiple stores — you may think the way Redux manages your app in just one object would bring about performance issues, especially as the object becomes increasingly large. The short answer is NO. Read this for details.
2. Define Action
In Redux, stores need to have big switch statements to do static type checks of actions using string
comparisons. Your application, as it grows, would become more and more difficult to maintain, due to the simple fact that a small typo could bring trouble.
So here, actions are defined as classes with the necessary arguments passed on to the constructor. This way we will benefit from the type checking; never again we will miss-spell an action, miss a required parameter or pass a wrong parameter.
Remember to extend the action from
Action
class thatstatex
provides. This makes your action listenable and dispatch-able.
3. Create Store & Bind Action
Simplicity is the key feature of this library. It uses the power of Decorators to bind a reducer function with an Action.
The second parameter to the reducer function (
addTodo
) is an action (of typeAddTodoAction
);action
uses this information to bind the correct action. Also remember to extend this class fromStore
.
Did you notice @Injectable()
? Well, stores are injectable modules and uses Angular's dependency injection to instantiate. So take care of adding store to the providers
list and to inject it into app.component
. Read Organizing Stores section to understand more.
4. Dispatch Action
No singleton dispatcher! Instead this library lets every action act as dispatcher by itself. One less dependency to define, inject and maintain.
Every action that
statex
defines are dispatch-able and listenable.
5. Consume Data
Consuming data is the crucial part. We don’t want UI to process a data if it hasn’t changed. @data
decorator uses a selector function to select a subset of the application state to work with. The property gets updated only when the value, returned by the selector function, changes from previous state to the current state. Additionally, just like a map
function, you could map the data to another value as you choose. It is compatible with TypeScript’s type checking.
Use
@data
decorator and a selector function (parameter to the decorator) to get updates from application state.
In this case todos
gets updated when state.todos
change. Life is not so simple always! We may need to derive additional properties from the data, sometimes using complex calculations.
Therefore
@data
can be used with functions as well.
Immutable Application State
To take advantage of Angular 2’s change detection strategy — OnPush — we need to ensure that the state is indeed immutable.
Immutable collections should be treated as values rather than objects. While objects represents some thing which could change over time, a value represents the state of that thing at a particular instance of time.
This library uses seamless-immutable — an immutable JS data structures which are backwards-compatible with normal Arrays and Objects — to keep application state immutable (comparison between seamless-immutable and Facebook’s famous Immutable.js library is here).
Since application state is immutable, the reducer functions will not be able to update state
; any attempt to update the state will result in error.
Therefore a reducer function should either return a portion of the state that needs change (recommended) or a new application state wrapped in
ReplaceableState
, instead.
Organizing Stores
Create STORES
array and a class Stores
(again @Injectable
) to maintain stores. When you create a new store remember to:
- Add the store to the
STORES
array - Inject to the
Store
's constructor
As you setup the project, add STORES
to the providers
array in app.module.ts
And finally, inject Stores
into your root component (app.component.ts
)
Making Your Code AOT Compatible
If you have used a version prior to v1.0.0
, this section will help you refactor your code to make it AOT compatible. The selector function to @data
decorator must be an exported standalone function, to avoid the below AOT error:
ERROR in Error encountered resolving symbol values statically. Function calls are not supported. Consider replacing the function or lambda with a reference to an exported function
Therefore refactor your code from:
@Component({
...
})
export class TodoComponent {
@data((state: State) => state.todos)
todos: Todo[];}
to
export function selectTodos(state: State) {
return state.todos;
}@Component({
...
}
export class TodoComponent extends DataObserver {
@data(selectTodos)
todos: Todo[];}
Remember to extend your class from
DataObserver
. It is essential to instruct Angular Compiler to keepngOnInit
andngOnDestroy
life cycle events, which can only be achieved by implementingOnInit
andOnDestroy
interfaces. Because of this constraint all components using@data
must extend itself fromDataObserver
which setsngOnInit
andngOnDestroy
properly;@data
in-turn depends on these functions. However if you would like to extend your class from your-own base class you may do so after making surengOnInit
andngOnDestroy
are implemented properly.
Sample Code
Sample code is here:
https://github.com/rintoj/statex/tree/master/examples/todo-ng-ts
This library is also available for React. Please check
Hope this article was helpful to you. Please make sure to checkout my other projects and articles. Enjoy coding!