Courier: a reactive React client architecture
With reactive programming / dataflow (RxJS, Bacon.js, Kefir) and immediate mode UI rendering (React, Mercury), we now seem to have all we need to create simple, functional-ish client architectures
Elm combines reactive programming and immediate mode rendering with static typing, immutable data, abstract data types, and a much neater syntax. Definitely worth a look if you haven’t checked it out already. If you can’t (or don’t want to) move to Elm, you can get some of the benefits of static typing from Flow or TypeScript and Immutable.js provides immutable data structures.
and I’ve recently grown fond of this one:
Leaving out URL handling for brevity, here’s a minimal example:
var Kefir = require("kefir");
var React = require("react");
courier = {};
courier.add = Kefir.pool();
courier.reset = Kefir.pool();
courier.messages = Kefir.merge(
[ courier.add.map(function() { return "add"; })
, courier.reset.debounce(750).map(function() { return "reset"; })
]);
var initState = { count: 0 };
function transition(state, message) {
switch (message) {
case "add":
courier.reset.plug(Kefir.constant(true));
return { count: state.count + 1 }; break;
case "reset":
return initState; break;
default:
return state;
}
};
var UI = React.createClass({
componentWillMount: function() {
this.props.states.onValue(this.setState.bind(this));
},
add: function(event) {
this.props.courier.add.plug(Kefir.constant(true));
},
render: function() {
return (<div>
<button onClick={this.add}>Click, quickly!</button>
<p>You are {this.state.count} clicks ahead.</p>
</div>);
}
});
var states = courier.messages.scan(transition, initState);
React.render(<UI courier={courier} states={states} />,
document.getElementById('app'));
The example repo has a version with comments and basic hash routing.
I find this easy to reason about and the implementation overhead is minimal so we don’t need a framework to use it.
We don’t need a framework for this, but we will want some libraries: setting up URL handling, routing in the transition function, passing dummy couriers to the transition function for testing, and a test harness for reactive networks, for example.
State transitions are confined to a pure-ish
The transition function isn’t quite pure since it makes calls to the courier and doesn’t enforce an immutable data structure for the state. The courier calls are all asynchronous however, and we can think of these instructions as being returned from the transition function rather than sent directly to the courier inside the function. I simply found the direct courier calls less verbose and without obvious downsides. I encourage you to to use something like Immutable.js for the state, but that’s not a requirement.
function which is comparatively easy to test and understand. The URL bar is treated in a principled manner as a special UI component.
I prefer to think of the URL bar as a special UI component that the browser renders for us. Thus, just like an input field sends its on-change instructions to the courier, URL changes result in instructions to the courier. The courier delivers this as a URL change message to the transition function, which decides what to do. The transition function could accept the new URL (and change other parts of the state accordingly) or simply reject the request to change URL and leave it as is. The transition function could change the URL at any transition and the URL bar will be updated to match the URL in the state generated by the transition function.
All asynchronous code is confined to the courier with a clear input (services) / output (messages) interfaces and a dataflow (reactive) implementation. This makes the asynchronous code easy to visualise (draw the reactive signals and transformations as lines and boxes), debug, and maintain.
I still have limited experience with this architecture and I’m not yet sure how it will scale to large web-apps. There should, however, be plenty of opportunity to modularise within the transition function, the UI rendering, and the courier respectively.
If you have any thoughts on this and in particular if you foresee any problems, I’d love to hear what you think.