In this post I will demonstrate how to create a Treeview component using React and Flux.
The React part is relatively simple and consists of two components; TreeView and TreeNode. It's worth noting that the TreeNode component is recursive in order to support n levels of nodes.
The code listing below shows the jsx representation of both components.
TreeView
var TreeView = React.createClass({
getInitialState: function(){
return {countries:NodeStore.getNodes()};
},
onChange: function(){
this.setState({countries:NodeStore.getNodes()});
},
componentDidMount: function() {
NodeStore.addChangeListener(this.onChange);
},
componentWillUnmount: function() {
NodeStore.removeChangeListener(this.onChange);
},
render: function(){
var countries = this.state.countries;
var nodes = countries.map(function(n){
return <TreeNode node={n} children={n.children} />
});
return(
<ul>
{nodes}
</ul>
);
}
});
React.render(<TreeView />, document.getElementById('treeView'))
TreeNode
var TreeNode = React.createClass({
toggle: function(e){
AppDispatcher.dispatch({
eventName: 'expand-collapse',
node: this.props.node
});
},
render: function(){
var nodes = this.props.children.map(function(n){
if(n.visible){
return <TreeNode node={n} children={n.children} />
}
});
return (
<li>
<span onClick={this.toggle}>{this.props.node.getIcon()}</span>
<span>{this.props.node.text}</span>
<ul>{nodes}</ul>
</li>
);
}
});
React is great and efficient for UI composition, but things can get complicated if you try to manage state and data flow directly in your React components. Instead it's often recommended to handle state management and data flow using Flux.
Flux is more an architectural idea than an actual framework. However, the idea basically boils down to pub-sub with some defined conventions. There are two key players – a dispatcher and one or many domain based “store” objects. The main role of the dispatcher is to delegate events from React components to the appropriate store.
As you can see from my example below the dispatcher is passing expand-collapse events to the NodeStore in order to expand or collapse nodes.
Strictly speaking there is no need for a framework to achieve this, but I have included some common helper libraries in the code listing below.
Dispatcher
var Dispatcher = require('flux').Dispatcher;
var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var _ = require('underscore');
var AppDispatcher = assign(new Dispatcher(), {
handleViewAction: function(action) {
this.dispatch({
source: 'VIEW_ACTION',
action: action
});
}
});
AppDispatcher.register( function( payload ) {
switch( payload.eventName ) {
case 'expand-collapse':
NodeStore.toggleNode(payload.node);
break;
}
return true;
});
Flux Store
It's the responsibility of the store to manage state and update the model based on UI events received via the dispatcher. The NodeStore is defined below:
var NodeStore = _.extend({}, EventEmitter.prototype, {
_nodes : createMockModel(),
getNodes: function(){
return this._nodes;
},
toggleNode:function(node){
for(var i = 0; i < node.children.length; i++){
node.children[i].visible = !node.children[i].visible;
}
this.emit('change');
},
addChangeListener: function(callback) {
this.on('change', callback);
},
removeChangeListener: function(callback) {
this.removeListener('change', callback);
}
});
Specifically the store will toggle child nodes on or off as parent nodes are expanded or collapsed.
Model
For simplicity I have added a mock model to provide a hierarchical model to power the treeview with a list of continents, countries and states:
function CountryModel(text){
this.children = [];
this.text = text;
this.icon = this.getIcon();
this.visible = false;
this.selected = false;
}
CountryModel.prototype.getIcon = function(){
if(this.children.length > 0) {
if (this.children[0].visible === false){
return '+'
}
return '-';
}
return null;
};
function createMockModel(){
var countries = [];
var america = new CountryModel('North America');
america.visible = true;
var usa = new CountryModel('USA');
usa.children.push(new CountryModel('New York'));
usa.children.push(new CountryModel('Texas'));
usa.children.push(new CountryModel('Oregon'));
usa.children.push(new CountryModel('South Dakota'));
america.children.push(usa);
america.children.push(new CountryModel('Canada'));
america.children.push(new CountryModel('Mexico'));
var europe = new CountryModel('Europe');
europe.children.push(new CountryModel('Norway'));
europe.children.push(new CountryModel('Sweden'));
europe.children.push(new CountryModel('France'));
europe.children.push(new CountryModel('Germany'));
europe.visible = true;
countries.push(america);
countries.push(europe);
return countries;
}