Reactive Components with RxJs filter-scan-map


This story describes a client-side architecture that applies to both web and native mobile applications. View Components are generating semantic events that are consumed by Logical Components, which in turn publish their (external) state as a stream. Working code is available as a Github repository: https://github.com/ReactivePatterns/ReactiveJsComponents.


Events and Components

The view and interaction logic are separated into what we call view components and logical components, which only communicate through events. The view components are converting platform specific events into semantic or logical events that are processed by logical components. View components are also subscribing to state changes that logical components are publishing.

Logical Components

A logical component is built from 3 pieces:

  • the initial state
  • an event processor that changes the internal state whenever an event arrives (Scala’s partial functions are an ideal implementation)
  • a mapper from the internal to the external/published state

For example:

function DoorComponent() {

var door = Stately.machine({
'CLOSED': {
'open': /* => */ 'OPEN',
'lock': /* => */ 'LOCKED'
},
'OPEN': {
'close': /* => */ 'CLOSED'
},
'LOCKED': {
'unlock': /* => */ 'CLOSED',
'break': /* => */ 'BROKEN'
},
'BROKEN': {
'fix': /* => */ 'OPEN'
}
});


return {
initialState: door.close(),
eventProcessor: function (door, event) {
console.log(door.getMachineState(), '->', event);
return door[event]();
},
publishedStateMapper: function (door) {
return {
'state': door.getMachineState(),
'events': door.getMachineEvents()
}
}
}
}

While a logical component can be internally implemented in an objected- oriented style, the filter-scan-map combinator sequence is a great alternative:

function LogicalComponent(name, logic) {
var publishedStateStream = Rx.Observable.return(logic.publishedStateMapper(logic.initialState)).concat(
eventStream.filter(componentFilter.bind(this, name)).map(function(ev) {return ev.event;})
.scan(logic.initialState, logic.eventProcessor)
.map(logic.publishedStateMapper));
return {
name: name,
getStateStream: function () {
return publishedStateStream
}
}
}

View Components

Facebook’s React framework is perfect for implementing view components because it is built to react to state changes that are passed as a single (immutable) object. All we have to do is “wire” view components and logical components so that every time a logical component publishes a new state, the interested view components are processing it in a standard way. We are using a mix-in that abstracts out this functionality, so that the view components are just dealing with view logic. Here is an example:

var DoorEvents = React.createClass({

mixins: [ ViewComponentMixin ],

handleEventClick: function(eventName) {
this.publish(eventName});
},

cssMapping: {
'close': 'moon',
'open': 'sun',
'lock': 'lock',
'unlock': 'unlock',
'break': 'settings',
'fix': 'wrench'
},

render: function () {
var links = this.state.events.map(function (event) {
return <a key={event} className="action ui button" onClick={this.handleEventClick.bind(this, event)}><i className={this.cssMapping[event] + ' icon'}></i>{event}</a>;
}, this);
return(
<div className="ui labeled vertical fluid icon">
{links}
</div>
);
}
});

What About MV*?

Both the logical and view components can maintain state, so they can be thought of as the model and the view-model pieces in the MVC or MVVM patterns. But it is really better to just think of them as event processors or stages in an event-driven architecture. The initialization logic that wires the components together can also be considered the controller, but again, the analogy is only partially helpful, as there is no logic beside the wiring in this stage.

var DoorStateView = require('./views/doorStateView.jsx');
var DoorEventsView = require('./views/doorEventsView.jsx');

var doorComponent = require('./components/doorComponent');

settings = {
logicalComponents: {
'DoorState': doorComponent,
'DoorEvents': doorComponent
}
}

React.renderComponent(
DoorStateView(),
document.getElementById('state')
);

React.renderComponent(
DoorEventsView(),
document.getElementById('events')
);

Final Thoughts

Using components as stages in event processing allows a clear decomposition of an application into reusable and maintainable pieces. Components can be composed using stream transformations in a way objects cannot. In particular, the filter-scan-map sequence can be thought of as a powerful replacement of objects.

In this example the component logic is pretty simple, but in general components would rely on other services or utilities to implement the event processing. In one of our future stories we will tackle the implementation of more complex logic using Behavior Trees as a task management framework.