A recent project of mine involved visualising a tree like data structure. Turns out React’s out-of-the-box support for recursive components makes this a really easy challenge. Here’s how you can build one.

For the impatient here’s what the end result looks like:

Let’s Start with the Data

The data itself is kept in memory as immutable objects using Immutable.JS. A reducer modifies the data according to two kinds of actions: “Add a new node” and “Expand/Collapse node”.

Each node is represented as an object with a text field, a list of children, expanded/collapsed state and index (called path). The index is kept as an array of indices from root down to the described node. An empty array is used as the index of the root item.

Initially we only have a single root node:

var initialState = Immutable.fromJS({ text: "root", childNodes: [], expanded: false, path: [], });

And we can add more nodes via the reducer:

function reducer(state = Immutable.fromJS(initialState), action) { var path; switch(action.type) { case ADD: path = pathForUpdate(action.payload.path); return state.updateIn(path, (node) => node.update('childNodes', (list) =&gt; list.push(generateNode(action.payload, list.size))) ); case TOGGLE: path = pathForUpdate(action.payload); return state.updateIn(path, (node) => node.update('expanded', (val) => ! val )); default: return state; } } function act_toggle(path) { return { type: TOGGLE, payload: path }; } function act_add(text, path) { return { type: ADD, payload: { path: Immutable.fromJS(path), text: text }}; } /** * Create the store and add some nodes. * Note the second argument to act_add is the path to * the parent node */ var store = Redux.createStore(reducer); store.dispatch(act_add("0", [])); store.dispatch(act_add("1", [])); store.dispatch(act_add("2", [])); store.dispatch(act_add("0_0", [0])); store.dispatch(act_add("0_1", [0])); store.dispatch(act_add("0_2", [0])); store.dispatch(act_add("1_0", [1])); store.dispatch(act_add("1_1", [1])); store.dispatch(act_add("1_2", [1])); store.dispatch(act_add("1_3", [1])); store.dispatch(act_add("1_4", [1])); store.dispatch(act_add("0_0_0", [0,0])); store.dispatch(act_add("0_0_1", [0,0])); store.dispatch(act_add("0_0_2", [0,0])); store.dispatch(act_add("0_0_3", [0,0])); store.dispatch(act_add("0_0_4", [0,0]));

Visualising With React Components

Now that all the data is in place we can proceed to creating the React components that show this tree. This is where React shines: A tree is a collection of nodes, so a single Node component will suffice to represent both container and children.

Recursion is baked into the framework, because we can use <Node /> inside the render() function of Node component to build the entire tree.

The code example assumes a toggle() property received from the parent to toggle the node’s state. This method is implemented in an external container component in order to decouple our Node from the Redux store. Below is the relevant code:

var Node = React.createClass({ toggle: function(path) { this.props.toggle(path); }, shouldComponentUpdate: function(nextProps, nextState) { return ! Immutable.is(nextProps.item, this.props.item); }, divStyle: function(item) { var indent = item.get('path').size; return { marginLeft: (indent * 10) + "px" }; }, renderChild: function(item) { return <Node item={item} toggle={this.props.toggle} /> }, render: function() { var item = this.props.item var disabled = item.get('childNodes').size == 0; var collapsed = ! item.get('expanded'); var text = disabled ? "" : collapsed ? "+" : "-"; return <div style={this.divStyle(item)}> <button disabled={disabled} onClick={this.toggle.bind(this, item.get('path'))}>{text}</button> <span>{item.get('text')}</span> {item.get('expanded') ? item.get('childNodes').map(this.renderChild, this) : false } </div> } });

Redux Big Win: Performance

React and Redux are a great fit because immutable data makes it very easy to implement shouldComponentUpdate. In case you don’t remember, this function is called whenever an element’s state or props change and before rendering it. We can use it to prevent rendering if the change is not relevant to this element.

Working with Immutable data means a simple comparison of the represented data object is all we need in order to know if the node or any of its children have changed.

Note the simple implementation of shouldComponentUpdate:

shouldComponentUpdate: function(nextProps, nextState) { return ! Immutable.is(nextProps.item, this.props.item); },

When any of the data objects change, all nodes from the root down to the modified data object will be re-rendered, and no other nodes are affected.

Scroll to the top of the post for the full example or head over and fork it directly in codepen:

