Adding Redux
In the previous section we explored constructing a TodoMVC application using angular directives which represented the different components of the application. In order for the components to communicate, we had to pass functions down to each child. The child could then use these functions to notify the parent of any state change. This approach works very nicely and it is a fantastic first approach when building an application.
As the application grows though, and more components are added, it likely will become apparent that callback functions just don't scale well. It gets quite cumbersome to continually pass functions and manually wire up the data transfer between a parent and it's children. Wouldn't it be really nice if there was a mechanism which could handle all of the application state and automate the process of updating the components on state change? Luckily, Dan Abramov and Andrew Clark designed an elegant solution for this exact problem! It's called redux. Redux takes the best parts of Flux and mixes them with interesting new patterns from languages such as Elm to give us a simple, yet powerful way to manage application state. Redux was originally built utilizing React for the UI layer. However, redux is written in pure JavaScript and is library/framework agnostic. This means we can use it with any UI frontend: Angular, Backbone, Ember, Knockout, React, etc. The module you'll use to hook redux into your application will be separate than the redux library itself. The react hook for redux is react-redux.
For this tutorial, since we've already been building the application using Angular 1.x, I will pull in some AngularJS bindings which were specifically written for redux. The project is called ng-redux and it will help us to hook redux into the AngularJS application.
The first thing we're going to want to do is install redux
and the AngularJS bindings, ng-redux
.
$ npm install redux ng-redux --save
Cool, now that we have these installed we can begin importing them into our application. There's really only a few main pieces we're going to need initially to get redux imported into our application. I'm going to import these pieces into the top level App controller.
import {todos} from '../../reducers/index';
import { combineReducers } from 'redux';
import 'ng-redux';
// ...
const ngModule = angular.module('todomvc', ['ngRedux'])
.config(($ngReduxProvider) => {
let reducer = combineReducers({todos});
$ngReduxProvider.createStoreWith(reducer, []);
})
.controller('App', App);
Note: If using TypeScript then you're going to need to make TypeScript aware of the redux
and ng-redux
modules. To do this, install them from tsd: $ tsd install redux ng-redux --save
Second Note: As a pre-caution, we're going to be using some ES6 features which TypeScript is not yet aware of out of the box. To circumvent this, install the es6-shim typings and include those in the project as well: & tsd install es6-shim --save
Alright! We should hopefully have made TypeScript happy and leaving us alone for the time being. ;) Just kidding, it can be frustrating to get a TypeScript environment stable at first, but once you get it settled, it is pretty nice. So, my advice is, don't give up on it if it gets tough.
Next, we'll need to add a reducer function to store the todos state. A reducer function is just a function which takes the previous state, the current action and returns the next state. It's signature looks like this:
(prevState, action) => nextState
In Flux, you may have heard about these things called stores. A store in Flux is a place where a single domain of data resides. It will be notified of actions from the Dispatcher and then can send change events out to UI Components who are subscribed. In Redux, the store is abstracted away from you. There is no Dispatcher, there are no change events in redux that you have to hook up. Instead, you comprise your entire application state into a single tree, or atom. Think of it like a giant JSON blob. This is different than traditional flux where you'll have data living in different stores. At the end of the day, what redux does is that on each action call it will call each reducer function with it's previous state and the current action. If the reducer is interested in the action, it reduces it's newState using the prevState + the action data and returns the result. Redux does this for every reducer you've told it about, and it takes the result for each reducer function and reduces everything into a new state tree (or atom, JSON blob). Then, it notifies all "connected" components of this update. The connected component can indicate which "slices" of the state tree it would like to have passed to it on update of the redux store. So, in essence, you have this waterfall design where all the application data is stored at the top and passed down through the UI tree. The philosophy is still quite similar to flux, however the methods used are quite different. If this made no sense or if you're interested to learn more, I highly suggest reading the official redux docs. They are absolutely fantastic. Read them and then read them again. You'll learn something new each time. Better yet, if you're an audio and visual learner, check out the new redux videos on egghead.io.
Ok, so coming back to integrating redux into this angular application. We have a simple TodoMVC app, and there's really only a certain number of things that a user can do using this app: Add todo, remove todo, edit todo, etc. These will be our actions. We'll represent out state using a single entity which will live in a todos
key in the state tree. Since we've injected the ngRedux
module into our app above, we're free to start using it in our components. Now, we can "connect" our components so that when redux updates the store, the component can get notified of the new data and have it passed along. We'll do that like this:
import * as TodoActions from '../../actions/todos';
class Component {
// ...
static $inject = ['$scope', '$log', '$ngRedux'];
constructor(private $scope, private $log, private $ngRedux) {
let unsubscribe = $ngRedux.connect(
this.mapStateToThis.bind(this),
TodoActions
)(this);
$scope.$on('$destroy', unsubscribe);
}
// ...
mapStateToThis(state) {
return {
todos: state.todos
};
}
}
There's a lot going on above, so let's take a minute and understand what exactly is happening. First, we're importing the set of TodoAction functions. In redux, an action most likely will be a function which returns a plain object. For our use case, it could look something like this:
import * as types from '../constants/ActionTypes';
export function addTodo(text) {
return { type: types.ADD_TODO, text };
}
// ...
Next, we're injecting $ngRedux
into the component so that we can call connect
on it to give it the slice of state we are interested in as well as the Actions that we would like bound to our UI controller. Notice that we are telling it which state we want via the mapStateToThis
method which is returning an object which is reading off the state which will be passed to it from redux after it updates the store. Now, what we have available to us is the actions are bound to the scope, so we call them from the view if we like:
<button ng-click="vm.addTodo(vm.inputText)" />
Pretty sweet right? Also, we have the todos key available on the scope as well. It will automatically be updated whenever redux updates the store. You can probably start thinking in your head of all the cool things you can do with this. It completely removes the need for us to pass Functions down to components and call them on a change. Now, all we need to do is call an action that is provided on the controller instance and it will travel to the reducer who returns it's result to the single state tree. Then all connected components get notified. This, in essence, is redux in a nutshell.
If you're an angular developer and you're wanting to try something new and exciting like redux, I hope this helped you out. It's reassuring to know that we can still use these new techniques in AngularJS today.
Cheers.