It has been a while since AngularJs was considered a modern framework. The latest version (currently 1.6.9) does not give us any substantial improvements and even its authors recommend transition to Angular as soon as possible.
While we were developing Admin — an application that uses AngularJs — there came a point when we needed to make a decision about the upgrade process.
We were considering two options:
1. develop a new application from scratch; or
2. continue with a more suitable solution — a hybrid application
Our decision was influenced by several different factors. For instance, some parts of Admin are still written in old versions of CoffeeScript. Also, we are trying to create new features in Admin all the time. Extensive refactoring is very rare, because we would have to allocate too much of our resources.
What is more, high priorities of the business are often in direct conflict with priorities of a programmer. It was for these and many other reasons that in the end we picked the hybrid application approach.
In a hybrid application you run both AngularJs and Angular at the same time.
This is why we came up with the idea to create a completely new feature in such application. However, we were concerned that it would only add new technical debt to our current situation. We knew that every change of this kind usually comes with a risk of new unknown problems (unknown unknowns).
How the upgrade started
We attempted the migration from AngularJs to Angular on multiple occasions. None of them was successful. In some attempts it was the lack of knowledge, in others it was caused by incomplete documentation on the side of Angular team. Also, there were various complications specific to Admin on top of that (for example custom Webpack configuration, multiple out-dated vendors and so forth). Our team realized that Admin was not ready for such change.
The last opportunity to upgrade emerged when we decided to renew one of Admin’s core features — the EventSettings module. The vision was to get rid of the buggy code although our main goal was to improve UX and UI. Therefore, we decided to rewrite this module completely.
It was a separate part of Admin and, if necessary, we could A/B test it on a small sample of the Slido user base easily enough.
Creating a root module
The first step was and always should be — an extensive research. A list of recommended publications is included at the bottom of this article.
And the outcome of our research? It was a few lines of code which represented a root module for our hybrid application:
Root module contains the original AngularJs module (line 2), new Angular module (line 10) and bootstrap of hybrid application (lines 22 and 26). And that is it. Simple.
Bootstrapping of a hybrid application is described in detail in guide provided by Angular team.
The above example shows how easily you can initialize all modules needed to run a hybrid application. But the reality is always more complicated than that. That is why the following part of this article will look at other commonly used files in this type of application.
Finding out about NgRx
In our previous article we described the problems we faced in relation to our custom implementation of FLUX architecture.
During the upgrade process, we considered trying some kind of a vendor which already implements FLUX, or which could help us with it. There were multiple options available, for example: Redux, NgRx, RefluxJs and so forth.
Change of architecture diagram
Now we will explain how the original diagram changed after using NgRx. Later, we will move on to particular changes in our application and compare it with our custom solution.
In implementation of FLUX using NgRx the data flows from components through Dispatcher to Reducers directly. Reducers are modifying parts of global state of application.
Instead of emitting an event about state change from Store, we have implemented Selectors, on which we can subscribe in components controller (we are using “reactiveness” of environment).
Selectors are methods mostly used for obtaining slices of store state. Therefore, business logic from Stores is now divided into Reducers and Selectors. Effects are used for handling of the so-called side-effects.
Effects listen for actions dispatched from @ngrx/store. They isolate side effects from components, allowing for more pure components that select state and dispatch actions. They provide new sources of actions to reduce state based on external interactions such as network requests, web socket messages and time-based events.
Effects will implement business logic which was in Actions before.
The following examples of EventsModule will be used to compare differences against our custom implementation described in our previous article.
Code examples FTW
The updated folder structure could be as follows:
I. View + Controller
As we can see, View has not changed much:
Perhaps the most important change is related to Actions from which the bindings for specific component action callbacks are removed. After the upgrade, we are calling them directly from controller of EventsEventItem component. This change was made because of our unwritten rule: there should be only one file for Actions per module. Same applies for Effects, Reducers, or Selectors.
Controller is also much simpler:
In our solution subscriptions replaced event handlers from the previous one. We are subscribing during initialization phase of component in constructor method and unsubscribing in ngOnDestroy method.
An interesting thing to metion is that the last action we call in the lifecycle of component is resetting of the current feature state for this particular module. Generally, this is considered good practice.
Actions is a list of actions which can be used in this module:
The main advantage of this approach si strong type control. Every programmer working on a big project would ceratinly appreciate this.
This is how so-called Effects can look like:
Effects are used for handling side-effect in data flow. In practice, it means for example, handling of asynchronous operations (AJAX calls, or other API communication). Business logic stored in Actions from our custom implementation is also moved there.
Effects require the highest entry effort from a programmer because, as they use mostly RxJs, they contain every part of the reactive programming paradigm. When a programmer overcomes this “obstacle” a big AHA! moment follows, after which everything becomes crystal clear.
IV. Reducers + Selectors
Last parts of our jigsaw are Reducers and Selectors:
Reducer is a pure function, that receives two parameters on input: the current application state and action that should be handled. Taking those parameters into account, it clones previous state and returns new updated state.
So what we did was moving business logic from Store from previous custom implementation to Reducer while “get” methods from Store were replaced by Selectors.
Now we want to glue all the pieces together, so we create EventsModule. See below.
Apart from common parts of the module, we need to register Services, Components and so forth. Here we registered also Effects and Reducers.
Specifics of hybrid application
Use of hybrid application comes with a multitude of specifics, one of which is registering of Effects. The following is our last example which shows how the root module looks after this change:
In the modified root module we have added initialization of Reducers and Effects. Effects for particular submodules in hybrid application are registered in a special way (line 40).
And finally, the last step is simple:
Sadly, you can’t find this approach in an official documentation. That’s why we had to study Github issues. However, this is likely to change in the future.
What problems does NgRx solve?
In the previous article we mentioned a few problems we faced with custom FLUX implementation. We had a problem with using of multiple stores. Also we considered our implementation as too excessive. And probably the biggest flaw was copying or cloning of the data. Now we will take a look if something improved.
Multiple stores: They were replaced by one global store, which is provided by NgRx. Selectors take care of handling special cases caused by asynchronous nature of application. For every substate we have implemented a dedicated Selector and whenever we need to use combined states, we use combined Selectors.
Those are memoized, so there is no overhead on application performance.
Too much writing: This has not changed so much. We maintain clarity of the code by following all the best practices recommended by authors of NgRx and RxJs. It can be easily seen in Effects, but also in the split of business logic to Reducers and Selectors.
Cloning the data: The problem with copying/cloning of object references is still there. But to prevent this sort of problems we are using great toolkit that comes with NgRx. One of those tools is ngrx-store-freeze (a metareducer), that uses Object.freeze() under the hood and notifies us about misusing the global state. Also we must mention Redux DevTools.
The migration to hybrid application was a good choice. Our decision to go with a custom FLUX implementation at the start helped us clarify what needed to be changed and what was to be kept during the migration process. NgRx clarified for us how code structure should look like. The well-beaten path helped us with the transition to reactive programming. And the results have definitely paid off.
In the next article we will show specifics and pitfalls of hybrid application in detail using real world examples.