From MartyJS to Redux

February 2015 we decided to build our UI on top of React and Flux. I had built systems using AngularJS before and decided I wanted something conceptually simpler and less opinionated in the large. Shortly after that time we chose MartyJS as our library of choice for implementing Flux. In August 2015 MartyJS decided to end support and the author moved to redux. Bummer.

Turns out the author had a good reason for moving to redux. A combination of a solid functional approach to the Flux stores and a sane method for implementing actions based on promises makes redux a very attractive way to do React. At this point I can't recommend it enough. It also clearly solved one of our long-running questions about MartyJS in particular and Flux in general: do you put API-communications logic in your actions or in your stores?

Fetching: Actions or Stores?

This problem deserves a little more explanation. Reading through the documentation on Flux it's clear that actions are the means by which the user initiates changes in state. Stores are obviously where the data for the application lives and the state that React will ultimately render. However, when a user, say, clicks a button and we need to send a POST request to a remote API to change some state on a remote server where should we put the logic? One could say that it belongs in the action - it's possible that the button click doesn't change any UI state directly and therefore will never even propogate to the stores. On the other hand, one could argue that actions should be very dumb and the store should be the final arbiter of all data and therefore it should seamlessly update remote APIs to reflect changes in its own data.

The further complicate the matter MartyJS provides State Sources which are Flux stores that are backed by either a remote API or local storage or session data, etc. This seemed to imply that the 'correct' thing to do is tie your stores to your API. That's the direction we went in for half a year.

That's the wrong direction.

You were expecting perhaps a road sign?

Your stores shouldn't tie to your remote API or you'll find yourself in an unfortunate tribute band

Why?

Well, here's what our code ended up looking like:

 1 var PrintToolbar = React.createClass({
 2   mixins: [WidgetStateMixin],
 3   handleClick: function() {
 4     ActionsPrint.pausePrint(this.state.print.printingJob);
 5   },
 6   render: function() {
 7     var job = this.state.print.printingJob;
 8     var loaded = job.status === 'request_pause' ||
 9       job.status === 'request_resume' ||
10       job.status === 'request_cancel';
11 
12       return (
13           <Loader loaded={!loaded}>
14             <Button onClick={this.handleClick}>Pause</Button>
15           </Loader>
16         );
17     }
18 );

This is UI code and it's pretty straightforward. We include a MartyJS-style mixin to get updates whenever the state changes via some kind of unknown black magic. We have a handleClick function because we don't otherwise have a very easy way to specify which job we want to pause (not strictly MartyJS's fault). When rendering we show a loading indicator based on our current status and show a Pause button. Neato-cheato.

What does our Action code look like for this pause?

 1 var ConstantsPrint  = require('widget/constants/print');
 2 var Marty           = require('marty');
 3 
 4 var ActionsPrint = Marty.createActionCreators({
 5   pausePrint: ConstantsPrint.PAUSE_PRINT(function(printingJob) {
 6     this.dispatch({
 7       actionType: ConstantsPrint.PAUSE_PRINT,
 8       printingJob: printingJob,
 9     });
10   }),
11   ...
12   // another 30 copies of almost the exact same lines...
13 });
14 module.exports = ActionsPrint

This is about as boilerplate as you can get. Why? Honestly there's not a great reason, this is just how MartyJS recommends you do it.

Just for fun let's look at how we create those constants:

1 var Marty = require('marty');
2 
3 module.exports = Marty.createConstants([
4   ...
5   'PAUSE_PRINT',
6   ...
7 ]);

You see, constants are a special MartyJS type that does a bunch of magic in tying together the actions, dispatcher and store. Basically when it comes down to it everything in MartyJS is pretty magical and it all works together to remove the boring details of doing Flux.

Alright, with that introduction you're ready to see some store logic.

I'll warn you, this isn't for the faint of heart.

 1 var StorePrint = Marty.createStore({
 2   displayName: 'Print',
 3   handlers: {
 4     ...,
 5     handlePausePrint                : ConstantsPrint.PAUSE_PRINT,
 6     ...,
 7   },
 8   handlePausePrint: function(action) {
 9     this.requestStatusChange(action.printingJob, 'pause');
10   },
11   requestStatusChange: function(printingJob, status) {
12     var url = URLBuilder.print("/job/%s/", printingJob.id),
13     error = function(response) {
14       console.error('Request to ' + status + ' the print failed.', response);
15       this.state.printingJobs[printingJob.id].status = 'error';
16       this.state.printingJobs[printingJob.id].error = response;
17       this.hasChanged();
18     };
19     Requests.put(url, { status: status }).then(function(response) {
20       if(response.status.code != 'ok') {
21         error(response);
22       } else {
23         this.state.printingJobs[printingJob.id].status = 'paused';
24         this.hasChanged();
25       }
26     }, error);
27   },
28 });

Alright, it wasn't that bad, to be honest. Again, we use Marty's internal magic to create the store which will do linking up via the handlers property between the different events that can get emitted and the various functions of our store that handle the changes. We've factored out requestStatusChange because several functions not shown here use it. We create the URL to request then use our own library, Requests to send the status update via PUT. We use promises to handle the response and the error. Because this is an older API it made the mistake of not encoding success or failure in the http status code and we have to check a separate status property in the returned JSON (the developers who made this design decision have been sacked).

This is pretty stock-standard for our original work on MartyJS - UI expects states, the Actions are dumb and passthrough, the dispatcher is a magic thing we don't think about or see and the stores hold all of the smarts. MartyJS seemed to suggest we build this way so we did.

Testing MartyJS

We believe very strongly in testing at Authentise. Most projects hover around 100% code coverage, we run tests against feature branches before merging pull requests, the whole bit. In retrospect we should have made testing a higher priority when selecting MartyJS. At the time I was sold on the quality of the documentation which was superior to most other Flux implementations at the time and the developer tools which were top-notch and really helped with debugging. Sadly, MartyJS didn't have a testing story when we started. People have since developed some good ideas, but the amount of magic inherently makes it difficult as you have to stub out something for testing that provides the same interface that each of Marty's components expect. We decided to soldier on without tests.

Don't do this.

Don't worry, the landing will slow them down

Sometimes the most direct route is not the safest route

Enter Redux

Redux is fantastic. Lots of other groups use it and it's received some pretty high praise. We love it because it removes a lot of the magic that MartyJS uses so that we can reduce our overall boilerplate. We can do this without sacrificing the developer debugging tools that we had gotten used to with MartyJS. The essential contention of redux is this: stores are pure functions that take in the previous state and return the new state of the application. That's it.

That neatly solves the question of what part of your logic talks to your remote APIs. Can't be your store. I said pure function.

So it has to go in your action. That's cool. But, then, what does an action look like? In MartyJS they were functions that call a dispatch function with information about an event to dispatch. Redux does things a little bit differently. Basic redux returns an action that will get dispatched. In order to do asynchronous requests we need to thow in the thunk middleware. This adds an additional layer of flexibility that is essential - your actions return a function that takes a dispatch paramter and calls that...ugh, this is getting ugly, let me show you some code

MartyJS:

 1 var ActionsPrint = Marty.createActionCreators({
 2   pausePrint: ConstantsPrint.PAUSE_PRINT(function(printingJob) {
 3     this.dispatch({
 4       actionType: ConstantsPrint.PAUSE_PRINT,
 5       printingJob: printingJob,
 6     });
 7   }),
 8 });
 9 module.exports = ActionsPrint

Redux

 1 var pausePrint = function(printingJob) {
 2     return function(dispatch) {
 3         dispatch({
 4             actionType: ConstantsPrint.PAUSE_PRINT,
 5             printingJob: printingJob,
 6         });
 7     }
 8 }
 9 module.exports = {
10     pausePrint: pausePrint
11 }

That's it, not that much different, just a little bit nested. The big advantage here is that we aren't using any special redux classes at this point. Redux defines an interface, but that interface is not hidden behind some class I inherit from. This makes the whole thing testable right out of the box. I can very easily write a test that ensures the above does what I want

 1 describe('printer actions', function() {
 2     it('emits the correct pause action'), function(done) {
 3         var fakeDispatch = function(event) {
 4             expect(event).toEqual({
 5                 actionType: ConstantsPrint.PAUSE_PRINT,
 6                 printingJob: 123,
 7             })
 8             done();
 9         }
10         ActionsPrint.pausePrint(123)(fakeDispatch);
11     }
12 });

Of course, the test isn't particularly sophisticated, but that's the point. We're just ensuring that we get dispatched with an event we expect.

Okay, so how do we go about doing our async calls? We'll just call dispatch a few times:

 1 var pausePrint = function(printingJob) {
 2     return function(dispatch) {
 3         var url = URLBuilder.print("/job/%s/", printingJob.id),
 4         Requests.put(url, { status: status }).then(function(response) {
 5             dispatch({
 6                 actionType: ConstantsPrint.PAUSE_PRINT,
 7                 printingJob: printingJob,
 8             });
 9         });
10     }
11 }

Not bad. What logic does that leave our store with? Well, remember that redux requires pure functions that return the new state when given the old state and the action that has taken place

 1 var reducer = function(state, action) {
 2   switch (action.type) {
 3     case ConstantsPrint.PAUSE_PRINT:
 4       var newState = _.assign({}, state);
 5       newState.printingJobs[action.printingJob.id].status = 'paused';
 6       return newState;
 7     default:
 8       return state;
 9     });
10   }
11 }

This is pretty simple. We switch on the action type, create a copy of the state and then update the printingJob in the new state. We can test this easily too

 1 describe('printer reducer', function() {
 2     it('updates the store when a print is paused'), function(done) {
 3         var pauseAction = {
 4             type: ConstantsPrint.PAUSE_PRINT,
 5             printingJob: {id: 123},
 6         }
 7         var newState = reducer({}, pauseAction);
 8         expect(newState).toEqual({
 9             printingJob     : {
10                 123         : {
11                     status  : 'paused'
12                 }
13             }
14         });
15     }
16 });

We send an event to pause the print and we end up with a store saying the print is paused.

The Migration

Alright, we've got the essential structure of how we accomplish what we want with redux and how it compares to MartyJS. What does the upgrade path look like. Here's the original plan we came up with

  1. Create a reducer that is parallel to an existing MartyJS store that can handle MartyJS-style actions
  2. Create an action that is parallel to the existing MartyJS action that can take any async logic from the old store
  3. Start migrating individual action types over to the new reducer
  4. Finish migrating all store logic over
  5. Migrate any lingering actions from MartyJS-style to redux-style actions

This seemed like a great plan on paper. It had several glaring problems in practice

  1. We didn't figure out how the UI is getting its state from the store. Redux uses props. MartyJS uses state and mixins.
  2. We didn't figure out how to get redux reducers subscribed to MartyJS events from MartyJS actions
  3. We didn't figure out how to get MartyJS UI elements subscribed to redux-style reducers

It would have taken a bunch of work to figure each of these things out. We're pragmatic - we don't have a bunch of time and money to dump into a migration effort. So here's our revised migration plan

  1. You touch a feature, you migrate it over if it isn't so painful that it will blow your schedule

This plan was actually 80% shorter than the original plan and came with incredible kick-the-can benefits.

I prefer management styles based on childrens games. Ask me about performance-review-via-sardines sometime

Never underestimate the power of development via recursively deferring pain

We first figured out how to get both redux and MartyJS able to supply data to React and our actual rendered elements. That could be a separate blog post itself. In the end we have a root component that can pass down props from the redux state and have child elements that can use mixins to get MartyJS state. These two systems cannot communicate with each other. IE, MartyJS actions only effect the MartyJS store and redux actions only effect redux stores.

Now when we hit a bug fix or a new feature our task list looks like this:

  1. Figure out what, if anything, we can reasonably port over to redux
  2. Plumb through, if necessary, from the root element to whatever elemnent we are working on so we can get the redux props we need
  3. Replace references to state with references to props
  4. Remove any references in the React element to MartyJS's store
  5. Rip apart the old MartyJS store handler into logic inside a matching redux action and the redux reducer
  6. Write tests that etch the behavior into stone

So far this process is working well enough. We tend to port over only leaf elements in our React element tree because they are small and easy and cheap. Occasionally we spend a day or two porting over something large. We have some large parts of the system with intricate interdependencies that we haven't approached yet. We would have loved to have a gradual solution, but since we didn't start with a pile of unit tests it made that path expensive and error-prone.

I wish I had a better story to tell you about the migration. I don't. The level of implicit contracts in MartyJS components and the magic just made it too costly to develop a solution where we could incrementally replace pieces at any level.

Redux Advantages

At the start of this post I said that redux allows us to makes doing actions via promises sane and reduces boilerplate. Then I showed you code that was roughly the same. What gives? Well, let's work on a slightly more complex example. Say I've got a button that pauses a print, same as before, but now I want to show an indicator that I'm working on pausing and then pausing is done. I need an action that 1) changes the state to 'working' 2) sends a remote API request 3) handles the response and changes the state to 'done'. In MartyJS that might look like this:

 1 var ActionsPrint = Marty.createActionCreators({
 2   pausePrint: ConstantsPrint.PAUSE_PRINT(function(printingJob) {
 3     this.dispatch({
 4       actionType: ConstantsPrint.PAUSE_PRINT,
 5       printingJob: printingJob,
 6     });
 7   }),
 8 });
 9 module.exports = ActionsPrint
 1 var StorePrint = Marty.createStore({
 2   ...,
 3   handlePausePrint: function(action) {
 4     this.state.printingJobs[printingJob.id].status = 'working';
 5     this.hasChanged();
 6     var url = URLBuilder.print("/job/%s/", printingJob.id),
 7     error = function(response) {
 8       this.state.printingJobs[printingJob.id].status = 'error';
 9       this.state.printingJobs[printingJob.id].error = response;
10       this.hasChanged();
11     };
12     Requests.put(url, { status: status }).then(function(response) {
13         this.state.printingJobs[printingJob.id].status = 'complete';
14         this.hasChanged();
15       }
16     }, error);
17   },
18 });

I've shortened it a bit for clarity. Now, by comparison, here's how we do the same thing in redux:

 1 var pausePrint = function(printingJob) {
 2     return function(dispatch) {
 3         var url = URLBuilder.print("/job/%s/", printingJob.id),
 4         dispatch({
 5             actionType: ConstantsPrint.PAUSE_PRINT_START,
 6             printingJob: printingJob,
 7         });
 8         Requests.put(url, { status: status }).then(function(response) {
 9             dispatch({
10                 actionType: ConstantsPrint.PAUSE_PRINT_COMPLETE,
11                 printingJob: printingJob,
12             });
13         }).catch(function(response) {
14             dispatch({
15                 actionType: ConstantsPrint.PAUSE_PRINT_ERROR,
16                 printingJob: printingJob,
17                 error: response,
18             });
19         });
20     }
21 }
 1 var reducer = function(state, action) {
 2   switch (action.type) {
 3     case ConstantsPrint.PAUSE_PRINT_START:
 4       var newState = _.assign({}, state);
 5       newState.printingJobs[action.printingJob.id].status = 'working';
 6       return newState;
 7     case ConstantsPrint.PAUSE_PRINT_COMPLETE:
 8       var newState = _.assign({}, state);
 9       newState.printingJobs[action.printingJob.id].status = 'complete';
10       return newState;
11     case ConstantsPrint.PAUSE_PRINT_ERROR:
12       var newState = _.assign({}, state);
13       newState.printingJobs[action.printingJob.id].status = 'error';
14       newState.printingJobs[action.printingJob.id].error = action.error;
15       return newState;
16     default:
17       return state;
18     });
19   }
20 }

That definitely doesn't look shorter or less boilerplate-y. Totally true. I lied. At first.

Let's assume we're following the flux standard action recommendations. Then we can package up our reducer a little better:

 1 var reducer = function(state, action) {
 2   switch (action.type) {
 3     case ConstantsPrint.PAUSE_PRINT_COMPLETE:
 4     case ConstantsPrint.PAUSE_PRINT_ERROR:
 5     case ConstantsPrint.PAUSE_PRINT_START:
 6       newState.printingJobs[action.data.printingJob].status = action.data.status;
 7       newState.printingJobs[action.data.printingJob].error  = action.data.error;
 8       return newState;
 9     default:
10       return state;
11     });
12   }
13 }

We just expect that the action is of the form

{
    type: 'some action type',
    data: {...}
}

If it wasn't an error action then action.data.error will just be undefined which our UI will handle properly by not displaying the error. Great. What about the action itself? We can make it shorter. First let's create a really nice helper function. This will produce a function that will return an action conformant to the standard

 1 import { ActionType } from 'constants';
 2 
 3 export function action(type) {
 4   return function(data) {
 5     let action = {
 6       type: ActionType[type],
 7     }
 8     if(data != undefined) {
 9       action.data = data;
10     }
11     return action;
12   }
13 }

With this we can create action creators :)

 1 import { action } from 'action-tools';
 2 
 3 var actionPauseStart    = action(ActionType.PAUSE_PRINT_START);
 4 var actionPauseComplete = action(ActionType.PAUSE_PRINT_COMPLETE);
 5 var actionPauseError    = action(ActionType.PAUSE_PRINT_ERROR);
 6 
 7 var pausePrint = function(printingJob) {
 8     return function(dispatch) {
 9         var url = URLBuilder.print("/job/%s/", printingJob.id),
10         dispatch(actionPauseStart({printingJob: printingJob}));
11         Requests.put(url, { status: status }).then(function(response) {
12             dispatch(actionPauseComplete({printingJob: printingJob});
13         }).catch(function(response) {
14             dispatch(actionPauseError({
15                 printingJob: printingJob,
16                 error: response,
17             });
18         });
19     }
20 }

That's a pretty solid improvement. Once you've written that pattern a few times you can actually factor it further so that you can do a PUT against an arbitrary resource and properly dispatch events to the reducer that update the resource data or updates an error message. Throw in some ES6 (which we strongly recommend) and you'll end up with action code that looks like this:

 1 var actionPauseStart    = action(ActionType.PAUSE_PRINT_START);
 2 var actionPauseComplete = action(ActionType.PAUSE_PRINT_COMPLETE);
 3 var actionPauseError    = action(ActionType.PAUSE_PRINT_ERROR);
 4 
 5 export function pause(job) {
 6     return dispatch => {
 7         var url = URLBuilder.print("/job/%s/", job.id),
 8         ActionTools.putAndDispatch(url, actionPauseStart, actionPauseComplete, actionPauseError);
 9     }
10 }

This code is about the same length as the original MartyJS action and yet it handles the full async request with progress events!

I want to highlight here that this is not necessarily a failing of MartyJS - there's nothing that was stopping us from building utility functions to remove boilerplate in MartyJS. But the architecture of MartyJS - using class heirarchies and expecting your code to interact deeply with those heirarchies - made such a task harder. Also, the error handling inside those class implementations tended to turn things as simple as syntax errors into silent failure to render. Redux uses very simple building blocks that make it easier for us to confidently build our own machinery to remove boilerplate while not hindering debuggability. And we haven't even touched on redux's ability to hot swap code or replay events.

Conclusions

We're really happy with redux. When we started looking at MartyJS we appreciated the direction it provided and it looked very promising, but it encouraged us to make some decisions we regret. When it was finally end-of-lifed we had to find something to replace it with and were glad to see the JS community had started to rally around redux which provides much better direction and more powerful tooling. I can't wait to see what happens when the migration is complete and we can take advantage of everything redux has to offer.