AngularJS migration — use redux to nuke $scope soup
Refactoring $scope soup with reactive(redux or rx) event stream components
Intro
—
This article helps you pave a way toward a successful code migration from your large scale Angular 1.x to Angular+. Same concepts can also be applied when migrating to react instead.
We’ve got you covered If you want to “Transition to React” instead.
Synopsis
—
The new Angular framework favors reactive based one-way data binding over dirty-checking-based bi-directional data binding.
To break away from $scope
, while keeping third party library integration, more efficient Zones were introduced. “A Zone is an execution context that persists across async tasks.” ~ angular/zones. This blog highlights techniques to remove dependency of $scope
while in Angular 1.x, to produce faster Angular ready code, without breaking things.
Nota: This is the 5th in a series of blog posts about soft code migration, as I move Hoo.gy — “a platform that helps you to rent various stuff to/from your friends, neighbours and coworkers” from Angular 1.x to Angular 2/4/5/X. Your recommendation (👏 👏 ) motivate to followup with a new article. Leave questions in comment section below, I will help however I can. Other articles are: First article, Second article, Third article, Fourth article, You are currently reading the 5th article. I hope this article helps you.
Observables
—
With Angular (2 and 4.x.x), the scope was removed completely. It is dead. Angular relies on context aware, asynchronous event based, change detector Zones as a replacement of digest cycle(synchronous) dirty checker.
$scope
—
Using scope’s dirty checking for bi-directional binding was the selling point for the then new framework. As you are aware of, this technique degrades performance as the codebase grows(or over 2k bindings). The lack of the model in MVC contributed state management nightmare, and hack-ish over-use of $scope.$watch
and $scope.$apply
. There is a better way: (Shared) States.
Shared State
—
One data store able to communicates change to subscribed agents, either via a pubsub or observer pattern(the cheaper the better) sums all about achieving single source of truth. Redux, is a light-weight throw-in library that just does that. Zones will handle events(later with Angular+), and Redux/Rx will manage application states(model) and synchronization.
Redux
—
Consistency and predictability of your code structure, will add long term value, for future code migrations or day-to-day give code refactoring. Redux is built on 3 main components: store as state keeper, reducers as state mutators and actions as message buses.
Store
—
The store serves single source of truth. It features subscribe()
and dispatch()
for Observer pattern implementation, and a getState()
to read the state. Since this concept is not native to Angular 1.x, there is a clear need to make it work with Angular: via a service.
# Initialization of State Service
function StateService(){
return Redux.createStore(Reducers, states = {});
}# Attaching the service to Angular 1.x
angular.module('app').factory('StateService', 'StateService');
Reducers
—
Redux is designed with one-way data flow at its core. Reducers are the only utilities allowed to mutate Store’s state. Quick reminder is mapReduce
computing concept, were the reduce operation(or combiner) derives a new states from initial mapped data sets.
# First reducer
function NotificationReducer(state, action){
switch(action.type){
case 'add:notification':
return Object.assign({}, state, {notification: action.value});
break;
}
}
# Second Reducer
function XyzReducer(state, action){}
function MessageReducer(state, action){}# Putting together all reducers
function Reducers() {
return Redux.combineReducers({
notification: NotificationReducer, xyz: XyzReducer
});
}
# Adding reducers to Angular 1.x
angular.module('app').service('Reducers', Reducers)# Initialization of State Service
function StateService(){
var initialState = {notification:{}, xyz:{}};
return Redux.createStore(Reducers, state = initialState);
}# Attaching the service to Angular 1.x
angular.module('app').factory('StateService', 'StateService');
Actions
—
Actions moves message payloads around. Think of them as message buses in redux sub-system. Every payload has a type and associated value. The type tells reducers what to, and the value is the actual data. Action Creators are factories of these payloads. Action creators on another hand, are utilities functions that helps to create actions.
# Typical Action function
function Actions(){ return { ADD_MESSAGE: 'add:message'}; } # Factory of Actions
function ActionCreator(Actions){
return{
addMessage: function(payload){
return {type: Actions.ADD_MESSAGE, value: payload};
};
}
# Attaching the Action Creator as service to Angular 1.x
angular.module('app').factory('ActionCreator', ActionCreator);
Patterns
—
Angular Component implementation gave birth to lifecycle hooks such as $onInit
, $onChange
or $onDestroy
to name a few. Some events that lead to state mutation range from HTTP Response (resolved promise), WebSocket message reception(Push Message), Web Worker or Service Worker Events, iFrame’s postMessage Event to name a few. These are common places to look for ways to restructure our code. When new data reaches our those parts, the reducers needs to mutate the state, and Store notifies subscribers about the change via dispatch().
Dispatch
—
# Use Cases
# When an HTTP Response promise resolves
.then((response) =>
StateService.dispatch(ActionCreator.action(Transformer(response)))
);# User triggered Action: Waiting spinner
# WebSocket(SocketIO) payload reception
io.on('/namespace', (payload) => StateService.dispatch(ActionCreator.action(Transformer(payload)))
);#iFrame message listener
window.addEventListener("message", (payload) => StateService.dispatch(ActionCreator.action(Transformer(payload)))
), false);# Catch-all Structure
var payload = ResponseTransformer.transform(data|event|message)
StateService.dispatch(ActionCreator.action(payload))
Events
—
Angular 1.x used $scope.$emit
and $scope.$broadcast
to dispatch custom events. Event handlers were handled by $scope.$on
callbacks. Most of these event emitters served to pass messages to components deep buried in Angular apps, that would otherwise work with $scope.$watch
construct. Quick example being a new chat message hitting WebSocket endpoint from server, a $rootScope.$broadcast(‘new:message’)
may have been used to notify other sub-systems concerned with the new incoming message.
$scope.$broadcast('state:x:changed', payload);
$scope.$emit('state:x:changed', payload);# Opt for static variables to name events in
$rootScope.$broadcast(CHANGE_EVENT);#If broadcast sends a payload, then the payload should be added.
Service.dispatch({type: CHANGE_EVENT, value: payload});
$scope.$on
—
Components concerned with an event implemented handler constructs to be used as “.$on” callbacks. Outside current context about state mutation. That makes them good candidate to be replaced with pub/sub(redux/ngrx). Custom events are good candidate to replace with subscribe. If event notifies about state mutation, then subscribe callback can easily update current state.
$scope.$[emit|broadcast]('state:x:changed');
$scope.$on('state:x:changed', updateStateX);
function updateStateX(payload){ vm.model.x = payload; }# Replacing Emitters/Broadcasters with dispatch(Action)
StateService.dispatch({action: 'state:x:changed', value: payload});
# Move state checker inside subscribe
StateService.subscribe(this.updateStateX);# The new updateStateX finds latest state from the Store
this.updateStateX = function(){
this.model.x = StateService.getState().x;
}
Takeaway is: Compared to $emit-ting or $broadcast-ing notifications about state mutations, It is more elegant to stream(dispatch) next state change events to shared state, and delegate the change to be committed by reducers.
$scope.$on(‘$destroy’)
—
Component implementation has lifecycle hook $onDestroy(or ngOnDestroy for Angular+), that can be be used for this special case.
# Cleanup on destroy the old way:
$scope.$on('$destroy', cleanup);# Cleanup on destroy the new way:
this.$onDestroy = function cleanup(evt){};
$scope.$watch
—
Most, if not all, “$watch
” constructs were designed to execute in case of state mutation. There is “ng-change
” that can help to prepare migration. The new Component has “onChange
” hook, that can be used for parent-child uni-directional binding. As a quick example, reacting to search text field change.
Using ngChange from Angular 1.3 to Angular 1.5
@link example https://stackoverflow.com/a/16501908/132610Using ngModelChange from Angular 1.x to Angular
@link example https://stackoverflow.com/a/36867467/132610
@link https://docs.angularjs.org/api/ng/directive/ngChange# in template
<input type=search ng-model=text/> # Angular 1.x directive $watch
$scope.$watch(text, (current) => {
_.debounce(() => MakeSearchRequest(current), 500);
});# new template
<input type=search ng-model=model.text ng-change=vm.search($event)/># Angular 1.5+ Component
this.search = function(){
_.debounce(() => MakeSearchRequest(vm.model.text), 500);
}# in Angular: only changes data binding
<input type= [(ngModel)]=model.text (ngModelChange)=search($evt)/>
$scope.$apply
—
Not really required, but callbacks that have been used inside $scope.$apply
can be decoupled for future use. The refactoring of Directive with Linking functions(jQuery plugins, etc) will be subject of the last article.
# Old way
$scope.$apply(function(){ /*.force something..*/ }); # New Way
$scope.$apply(this.forceSomething);
this.forceSomething = function(event){
/*.force something..*/
}
WebSocket
—
Streaming WebSocket messages to redux managed store.
#Initialization of WebSocket(with SocketIO)
# Good candidate for App Component's this.$onInit
var params = {query:['token',$auth.getToken()].join('=')};
this.socket = io(Config.WS_URL, params); this.socket.on('message', _.debounce(this.onMessage, 500)); // this.socket.on('error', this.wsErrorReporter); this.socket.on('connect_error', this.wsOffline);#Definition of Event Handlers
this.onMessage = function(message){
var payload = MessageService.format(message);
StateService.dispatch(ActionCreator.addMessage(payload));
}
this.wsErrorReporter = function(error){}
this.wsOffline = function(error){}
Services
—
How services can be improved or refactored? That is subject to next article in making. Meanwhile, It worth to mention that this pattern will occur more than once.
function HttpService(){}
#
function ChildService(HttpService){
# Example of function
function getUsers(){
return HttpService
.getUsers()
.then(function(response){
StateService.dispatch(ActCreator.action(process(response)));
return response;
});
}
# More functions.
}ChildService.$inject = ['HttpService'];# Adding Code to Angular 1.x
angular.module('app').service('HttpService', HttpService);
angular.module('app').service('ChildService', ChildService);
Component
—
The last pattern worth to mention is the redux code in Components. The rule of thumb is to have a model, that can be updated every time there is state mutation. The destructor will de-register from such updates when the component dies.
# Pet Component
function AwesomeComponent(StateService, ChildService){
# Temporal, but important.
var vm = this;
# tracking our subscriptions.
this.$onInit = function(){
ChildService.getUsers(); # fan of async/awaits?
this.unsubscribe = this.subscribe();
};
# Cleanup the mess
this.$onDestroy = function(){
if(typeof this.unsubscribe === 'function') this.unsubscribe();
this.unsubscribe = null;
}; this.unsubscribe = this.subscribe();
};
# Cleanup the mess
this.$onDestroy = function(){
if(typeof this.unsubscribe === 'function') this.unsubscribe();
this.unsubscribe = null;
}; # The actual subscription
this.subscribe = function(){
return typeof this.unsubscribe === 'function' ? false : StateService.subscribe(function(){
vm.model = StateService.getState().users;
}); # Ends state.subscribe } # Ends this.subscribe # Adding more implementations + Including ChildService.getUsers()} # Ends Component
Conclusion
—
Angular 1.x impact performance of large scale apps. ControllerAs
notation introduced in Angular 1.3 was in part to tackle performance issue, by reducing the number of functions directly attached to the $scope. Likewise, to offload state management off $scope
, redux improve overall performance while increasing readiness to embrace a new framework. Since you are planning migration to Angular (2/4/5 any x really), less logic bound to scope translates into saving time and money.
Additional reading material.
- Redux is well documented, and has a free course on egghead.io, both by the library creator Dan Abramov
- There is also a tutorial on how to use Redux for UI with Angular 4 and this one from Auth0 about “Managing States with ngrx/store”
- NgRx: Patterns and Techniques
- Redux Form Validation
- Full **s book on React/Redux tips
Closing note
—
Gosh, You rock If you read all above goodness! Thank You! Like last time, YOUR recommendation(👏 👏 👏) motivates me to follow up this post with techniques I use while upgrading Hoo.gy — a platform that makes it possible for you to rent stuff from your friends and neighbors. It is green sharing economy, to curb consumerism and save you thousands in credit card debts, and our planet ;-) … Also join the waiting list, I never send emails unless there is really a very big announcement!