Angular — building Apps using Flux

How to integrate Flux in your AngularJS Applications

Kris Temmerman

Flux is an architecture made famous by Facebook and React. It is based on known design patterns like the Observer and the Command Query Responsibility Segregation (CQRS), so it provides a solid base to create complex applications.

In this article we are going to use a basic shopping cart application to showcase Flux.

Use the links below to hack the final solution:

Demo | Source

Follow me on Twitter for latest updates @gerardsans.

Introduction to Flux

Before going into details, let’s briefly list the potential benefits of using Flux with Angular:

  • clear separation between application-state and ui-state
  • standardised approach, nice to have for complex projects
  • predictable, protection from side-effects due to cascading two-way data-bindings

Not every project can benefit from Flux so it’s a good idea to evaluate it before adopting it for your project.

Flux main goal is to provide a one-way data flow
Flux unidirectional flow

Let’s see what are the responsibilities for each component on the diagram above.

Actions

These represent actions, either originated by user interactions or the server, that are going to interact with the underlying data, called stores. Action Creators are components containing helper methods that dispatch specific actions. These helper methods just prepare the payload for the Dispatcher.

Asynchronous operations are handled using actions.

Dispatcher

The Dispatcher acts as a central hub for all data flow. It’s main goal is distributing the actions payloads to stores. It’s basically a message system. Every store has to register itself using a callback. All registered stores are notified on every action using this callback.

The main methods are:

  • dispatch(object payload) — Dispatches a payload to all registered callbacks. Used on Action Creators.
  • register(function callback) — Registers a callback to be invoked with every dispatched payload. Used on stores.

You can find more information of Facebook’s Dispatcher here.

Stores

Stores are singletons containing the application state and logic for a specific domain. This can be viewed as a traditional model in MVC but is not restricted to a single object. Stores emit changes to the views using events.

Stores should only handle synchronous operations.

Views

These are the regular views in MVC. In an Angular context, the view will be always paired with a scope. Views can either initiate actions or read the state from stores after being notified. Views register to store changes to show updates.

Flux dependencies

In our example we are going to use the following dependencies:

  • Dispatcher — Facebook Dispatcher included in Flux. We wrapped the Dispatcher into a Service for convenience inside a Browserify standalone bundle.
  • EventEmitter — Node’s implementation of events. Required to communicate changes to the View from the Store. Note we can’t use a Service in this case as we need a different instance for each store. Also note we could use $broadcast/$emit but store events are independent and not related to scopes in any way.

Include these dependencies to your index.html or application bundle.

Shopping cart example

Let’s create a basic shopping cart functionality to demonstrate how we can integrate Flux with AngularJS. The application will allow us to add and remove items while calculating the total.

We will start with the CartStore. This store will hold the list of items the user has added together with its quantity.

CartStore

See below the general structure for our store. We will use the EventEmitter to communicate changes to the view. First we will register a callback on the Dispatcher so we can respond to actions triggered by the user.

module
.factory(“CartStore”, function (Dispatcher, Actions) {
var event = new EventEmitter(),
dispatchToken,
cartItems = [];
// Dispatcher Callback Registration
dispatchToken = Dispatcher.register(function(action) {
switch(action.type){
case Actions.CART_ADD_ITEM:
addItem(action.data);
//notify change
event.emit('change');
break;
...
}
});
 // public read-only API
return {
getItems: getItems,
event: event,
dispatchToken: dispatchToken
};

//helper functions
function addItem(item){}
function removeItem(item){}
}

The action.type must match the payload on the Action Creator. In order to do that, we will create an Actions service to hold all the action tokens for the application.

module
.factory("Actions", function () {
var options = {
CART_ADD_ITEM: "CART_ADD_ITEM",
CART_REMOVE_ITEM: "CART_REMOVE_ITEM"
};
return options;
});

After calling addItem we will notify the view emitting a change event with event.emit. Note that the ‘change’ token must match the one in the View.

Stores API are always read-only and composed by the method to retrieve the state getItems together with the supporting event and dispatchToken (optional, used to set precedence for stores if necessary).

These are the main methods of our store.

function addItem(item) {
var items = cartItems.filter(function(i) {
return i.data.id == item.id;
});
if (items.length === 0) {
cartItems.push({
qty: 1,
data: item
});
} else {
items[0].qty += 1;
}
}
function removeItem(item) {
var index = cartItems.indexOf(item);
if (index>=0) {
cartItems.splice(index, 1);
}
}

When adding an item, we check if it’s already on the shopping cart, if not, we add it; otherwise we increment its quantity. When removing an item, we will remove it from the cart altogether.

CartActions

Our CartStore will receive specific actions dispatched from CartActions, an Action Creator module.

//Action Creator Module
module.factory(“CartActions”, function (Dispatcher, Actions) {
return {
//specific action
addItem: function(item) {
Dispatcher.dispatch({
type: Actions.CART_ADD_ITEM,
data: item
}
);
}
};
});

For the addItem action we implemented Dispatcher.dispatch passing the action type and the item as the payload. As before, we used the Actions service to facilitate maintaining unique tokens throughout the application.

👮 run asynchronous operations in Action Creators

View

Our view will be composed by two sections:

  • Product catalog — product list
  • Shopping cart — products and quantities added plus the total

We will be able to add products to the cart or remove them.

From our controller we will trigger the actions and receive updates from our store. This is a simplified version for the catalog section.

// index.html
<h1>Catalog</h1>
<div ng-repeat="item in mc.catalogItems">
<span>{{item.title}}</span>
<span>{{item.cost}}</span>
<button ng-click="mc.addItem(item)">Add to Cart</button>
</div>

When the users clicks on ‘Add to Cart’ it will invoke addItem in the controller. We create a copy for the item to avoid changes from the store.

// trigger addItem action
function addItem(item){
CartActions.addItem(angular.copy(item));
}

In order to be notified of updates we will register a callback to update the cartItems array being used to display the cart items. Find the simplified version for the cart section below.

<h1>Shopping Cart</h1>
<div ng-repeat="item in mc.cartItems">
<span>{{item.data.title}}</span>
<span>{{item.qty}}</span>
<span>{{item.qty*item.data.cost}}</span>
<button ng-click="mc.removeItem(item)">x</button>
</div>
<div ng-if="mc.total">Total: {{mc.total}}</div>

In order to respond to changes we used the EventEmitter instance in our API. The ‘change’ token must match the one used on the store. For consistency we recommend using the same for all stores.

// register callback to respond to updates from store
CartStore.event.on('change', cartUpdated);
// deregister on $destroy (clean-up)
$scope.$on('$destroy', function(){
CartStore.event.removeListener(cartUpdated);
});
function cartUpdated(){
vm.cartItems = CartStore.getItems();
vm.total = vm.cartItems.reduce(function(last, item){
return last + (item.qty*item.data.cost);
}, 0);
}

Any time the cart is updated, the change event will be triggered. On our callback we retrieve the latest cart state and calculate its total.

🐒Use Array.reduce to calculate things the functional way

We covered all the main blocks to build a basic Angular Application using Flux. Hope you are now able to understand Flux and maybe use it in your next project. The final solution is available here.

Thanks for reading! Have any questions? Ping me on Twitter @gerardsans.

Resources


One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.