Recently I've been lucky enough to start out on a greenfield project, (and it's a whopper!) lucky lucky me! Now although there's a lot of nifty techniques being used, there's one that I'm really excited about: our data is coming through as JSON :D. We get to write a "smart" front end! Not the whole site though, that would be a but much, just the more "app-like" areas. This means we're going to have to handle some state, and if you're following fashion that means REACT... which is amazing! But, possibly a little overkill. There's a lot we don't need, but also a couple of parts I'd really really like: flux and a single state object.

Enter Redux. ~700 lines of JS (unminified & with comments) that give us state handling like flux (but streamlined) and the niftiest state object I've ever seen. React and Redux are both new to me, Redux more so. So, I ran through a few tutorials, this one particularly stood out, and put together the most minimal (while remaining actually useful) implementation of Redux to get my head around it and hopefully allow the other FEDs on the team to decide if it's worth bringing into a production project. The following is that demo, refactored a little for publishing as an article, but you can find the original demo files on github. It is an attempt to tell the story of an Action in Redux. I hope you like it.


Redux! It only deals with application state which is held in a single object - "the store". Although it's an object, we don't access it directly. Instead we have to ask it for itself eg store.getState(); or fire an action to make updates, eg: store.dispatch(an action); We can also subscribe to the store to get notified of state changes, eg: store.subscribe(a callback). The following is a high level overview of how it all works:

/*  -------------------------------------------------------------------
 * | Everything inside this ascii box is our other code, Redux       |
 * | doesn't care about this bit. Here we handle user interactions,  |
 * | browser events, server events, DOM changes, asking Redux to     |
 * | update state and receiving state updates from Redux.            |
 * |                                                                 |
 * |        Our subscriber                   Our dispatcher          |
 * -------------------------------------------------------------------
 *                ↑                                ↓
 * -------------------------------------------------------------------
 * |           Reducer    ←    Middleware   ←   Action               |
 * |                                                                 |
 * | This ascii box is Redux.                                        |
 * | Actions comes in, they have a type eg 'SET_NAME' and sometimes  |
 * |    data eg 'bob'.                                               |
 * | Middleware functions are run on every action before it hits the |
 * |    store, we can have 1, none or many. A middleware function    |
 * |    might do something like capitalize a name: 'Bob', or log the |
 * |    action, or do some kind of validation before the action hits |
 * |    the heart and soul of Redux:                                 |
 * | Reducers are functions built into the store that handle state   |
 * |    updates. But - they don't update the state directly. Instead |
 * |    the store creates a new state object. This new state object  |
 * |    is mutated with whatever the reducers return. This new state |
 * |    object is now the current state object. When all the reducers|
 * |    are finished - all the subscribers are informed.             |
 * -------------------------------------------------------------------

Just to clear something up, the reducers and middleware functions are what's known as pure functions. They don't reach out and modify anything, they receive something, tweak that something and return it. Like a math function. Redux doesn't enforce this, it's up to us to keep to the principle.

It’s a coffee grinder: beans go in, powder comes out, end of story.

That covers the overview, now into Redux we go.


Actions

All information is passed through actions. These are objects that can take any form but having a standard seems like a really good idea. There's even an action compliance test on that repo to ensure the standard is adhered to. Here's a JSON description of what a standard action would look like:

{
    type: String,
    payload: Any,
    error: Boolean,
    meta: Any
}

Before we get into the action, it's (apparently) good to set up a central list of action types. We could use just plain strings to do this but there are some good reasons laid out here to do the following:

const USER_ACTIONS = {
    CREATE: 'CREATE_USER',
    SET_NAME: 'SET_NAME',
    DELETE: 'DELETE_USER'
};

Now the action itself. For each action we create a function that will build the correct structure. This makes our life much simpler when we get to firing off actions from other parts of the project.

var AsyncSetNameActionCreator = function (name) {
    return function(dispatch) {
        setTimeout(function () {
            dispatch({
                type: USER_ACTIONS.SET_NAME,
                payload: name,
                error: false
            })
        }, 2000);
    }
}

To deal with asyncronus requests we return a function which you'll note has been passed dispatch. That allows it to dispatch the action by itself when the async request completes. For ease in setting up the demo, the "async" above is stubbed with a timeout, how very fancy! Note that normally actions are passed straight through to the reducers which only accept objects - "but we're returning a function!" I hear you cry, "how can this be if reducers will not accept such things?!?" - so we need a bit of middleware called "thunk", that'll hold onto this function until it dispatches the action within the timeout which happens to be, you guessed it, an object. And with that nice little segue, lets move onto:


Middleware

These have a crazy looking structure, for those in the know, think Currying - it's a concept I've yet to get my head into so I'll just stay away from that for now.

//First level
 var middleware_1 = function ({ dispatch, getState }) {
    //Second level
    return function(next) {
        //Third level
        return function (action) {
            //this is thunk which I mentioned above for dealing with async functions, it's tiny!
            return typeof action === 'function' ?
                action(dispatch, getState) :
                next(action)
        }
    }
}
  • First level: provides dispatch & getState to the next two levels to use.
  • Second level: provides next which we can return to move on to the next step in the process (think express.js - or is that native to node? Can't remember, one of the two).
  • Third level: provides the action fired from the original dispatch or from any previous middleware that may have run before the current piece.

Each piece of middleware is registered with redux through applyMiddleware(), we'll get to that later. There's a lot of pieces of middleware that have been written by others and shared here if you're interested in some examples or ideas of what might be done here.

Here's another one that you might find useful, it's the simplest of the simple:

 var middleware_2 = function ({ dispatch, getState }) {
    return function(next) {
        return function (action) {
            //it does log!
            console.log('middleware_2: action received: ', action);
            return next(action);
        }
    }
}

Reducers

We have arrived at the heart of Redux - these are the guys that create the state changes. We could have one big Reducer to handle the entire state object and deal with every type of action but it makes more sense to break it out into a collection of simpler Reducers - each handling only a part of the state object. We'll assign each Reducer to it's specific bit of state in the next section. First we need to put one together.

Each Reducer receives the bit of state it's responsible for, and the action that originally kicked off the whole process. They then create a new object which is basically a copy of their bit of state and mutate that copy based on the action received. That object is returned and used by Redux to build the next version of the whole state object. Remember that note from before about 'pure functions' - this is that.

var userReducer = function (state = {}, action) {

    switch (action.type) {
        case 'SET_NAME':
            return {
                name: action.payload
            }
        default:
            return state;
    }
}

The above reducer (userReducer) is in charge of the user section of the state object. In this demo, the user section has a key: "name". So this reducer returns an object with the key "name" and the value passed in by the action - within it's payload - check AsyncSetNameActionCreator(name) to remember how it built the action object with the correct structure. By doing this the reducer is helping Redux set up the next version of the whole state object which will delivered to our subscribers at the end of the process.

Here's another one for fun! Haven't tested it though - looks like it might just replace the items array

var itemsReducer = function (state = [], action) {

switch (action.type) {
    case 'ADD_ITEM':
        return [ action.item ];
    default:
        return state;
}

}


Redux

Up until now all we have done is declare variables independently of each other. We've made an action, some middleware and a couple of reducers. Now we can string them all together!

To start we have to pull in the various bits of Redux that we need. In this demo I've brought it in through npm and am using Browserify to provide it client side.

//the ability to instantiate THE store
var createStore = require('redux').createStore; 

//the ability to insert middleware
var applyMiddleware = require('redux').applyMiddleware;

//the ability to assign each reducer to a specific part of the state object
var combineReducers = require('redux').combineReducers; 

Now we have all the bits we need we can take the first step of wiring it all up! This first step instantiates a blank store and registers each piece of middleware we wrote within it. Note that the order of the middleware here counts, this is the order in which they will run.

const middlewareBuild = applyMiddleware(middleware_1, middleware_2)(createStore);

The second step is to put together the high level structure of our state object. Note that the values being passed are the two reducers we created above. This is the point at which we are declaring their area of responsibility.

var reducerBuild = combineReducers({
    user: userReducer,
    items: itemsReducer
});

In the third and final set up step, we pass the reducer object from step 2 to the empty store with which we registered our middleware from step 1. The resulting "store" is our Reduxifyed state object. This is the guy we ask for state updates, for the state itself, and to which we send our actions.

var store = middlewareBuild(reducerBuild);

Now we play!

If you remember the ascii boxes from the beginning, the following code would be in the top box. Our user interactions, browser events, server events, DOM changes, etc. In the repo I have a simple index.html file with an input, a button, and a p tag. The first little bit of script fires a "set name" action with the value of the input. The second bit subscribes to any store updates and sets the value of the p tag when that happens.

document.getElementById("go").addEventListener("click", function(){
    var inputVal = document.getElementById('input').value;

    //our dispatcher
    store.dispatch(AsyncSetNameActionCreator(inputVal));
});

//our subscriber
store.subscribe(function() {
    var newState = store.getState();
    document.getElementById('result').innerHTML = newState.user.name;
});

If you download and set up the demo you can try this out yourself - have a look in the console, I've put in a bunch of console.log statements to try and illustrate the process as each step happens.

So that's it! If you remove all my comments from the code, you'll see Redux really doesn't need much boilerplate at all. It's brilliant! Granted I have yet to use it in more than just a demo but the potential is clear to see, especially when the author is putting together such phenomenal examples as this presentation.

Even if Redux doesn't get incorporated into the project I mentioned at the very start, it is most definitely going to be a feature in my future work - I'm actually tempted to refactor some existing code with Redux as the state handler. The gains it will afford are just too good to pass up! If you have any demos or, even better, an example of Redux in a production project - let me know! I would love to dig in and how it's working!

Future edit - it's been a few months since the start of the greenfield project I mentioned earlier, we didn't use Redux and I'm kind of hurting, sigh. Well - lessons learned!


1,450 0 17