Webinterpret's main application, which currently supports eBay and Amazon sellers, managing their products, stocks and shipments, is created in AngularJS.
When we started working on it, we used the standard component-based architecture.
Our application had four basic layers:
But, as every application, it started to grow. We created feature after feature, more and more services and controllers for each new view.
Things started to get complicated when we had controllers that depended on multiple services which, in turn, also needed data from other services. The code became messy and hard to manage. Syncing data models was a nightmare.
Starting from this:
It quickly became this:
As you can see, that's not very clear to understand. Not to mention representational components which also need to have and manage their own state.
Syncing state between different controllers also started to be painful, we had to resort to broadcasts to tell other controllers when to update their states.
So we decided to turn to Redux.js.
What is Redux?
Redux is a library which isolates application state and keeps it immutable in one place. And that's basically it. Sounds simple, but what exactly is state?
State is a simple object, a single point of truth accessible from the whole application. It can store whatever data is needed in application life cycle, such as user data, settings, products list, orders and much more.
Redux was originally created for React applications but it's written in vanilla javascript, so it can be used by any front-end framework.
I won't go very deep into Redux itself because there are a lot of tutorials and articles on the internet, so you can easily find it.
Right now I want to focus on real example of data flow with the usage of Angular and Redux. Let's just take one example controller and look deeply at what we did there.
So how to use it with Angular?
Angular has it's own binding for Redux - a library called ngRedux. It contains everything needed to handle state in Angular.
First of all, we had to create actions and reducers which will manage our state. Actions are functions that pass simple objects to reducers. Each action has it's own type defined as a string. Reducers are the only ones who can manipulate the state. They are executed after dispatching an action and, based on passed action type, change the state and return new one.
The real example - products list
Let's now focus on a real example of data flow using Angular and Redux. We will take a closer look into a list of user's products.
Step 1.
Page loads - products list is empty
After page loads, global application state is initialized. As a default, productsList
is an empty array and it's stored in our state.
const defaultState = {
productsList: []
};
Step 2.
Controller dispatches an action to fetch products when needed
Controller is connected to that part of the store, which is used in this particular view. First of all, we have mapStateToThis
function, which returns productsList
from the state.
function mapStateToThis(state) {
return {
productsList: state.productsList
}
}
Then in our constructor we connect the result of this function to this
.
class ProductsCtrl {
constructor($scope, $ngRedux) {
let unsubscribe = $ngRedux.
connect(mapStateToThis)(this);
$scope.$on('$destroy', () => {
unsubscribe();
});
}
}
From now on, whenever the state changes, we will get it in this.productsList
.
After the page loads, controller checks if there is a need to fetch products. If productsList
is empty, controller dispatches proper action to fetch it.
$ngRedux.dispatch(productsActions.fetchList());
Step 3.
Action connects to API and fetches the data
Action uses $http object to get the data from API.
export function productsActions($http) {
function fetchList() {
return (dispatch, getState) => {
return $http({
method: 'GET',
url: SOME_URL
})
.then(products => dispatch(
setProductsList(products)
))
.catch(() => {
// error handling
});
}
}
// more actions
}
Step 4.
Action passes an object to reducer
After fetching data, fetchList
dispatches another function (in this example setProductsList
) which takes the data, adds the type and passes it to reducer.
function setProductsList(products) {
return {
type: 'SET_PRODUCTS_LIST',
products
};
}
Step 5.
Reducer puts products into state and returns it
Reducer checks every action type which comes from actions, through a switch statement. Based on this, it executes state manipulations.
case 'SET_PRODUCTS_LIST':
return _.extend({}, state, {
products: action.products
});
When the reducer receives SET_PRODUCTS_LIST
, it extends state.products
with a list from action.products
.
Step 6.
Controller receives changed state
Thanks to $ngRedux.connect
, our controller already has the newest state in this.productsList
. And, since actions dispatchers are promises, we use them to do more actions after our state is changed.
$ngRedux.dispatch(productsActions.fetchList())
.then(() => {
// something happens
});
Summary
After introducing Redux our app architecture became much clearer:
Redux is a great tool to manage your application(s) and, as you can see, it can be used in any type of application - not only in React. It allows you to sync your data models easily, across the whole application. It's also very safe, as there is limited access for state manipulation.