How to improve the data flow in your React app? Introduction to Baobab
One of the reasons why we love React is its declarativeness. Instead of thinking how to change things, you simply declare how it should look like, and React does the dirty job for you.
However, as your application grows, it might be hard to keep up with the data flow. Especially when it has a complex structure: many layers, nested components and things like that. The state of your components is internal. The more complicated structure you app has, the more props- and callbacks-passings you need to keep your components up-to-date. It starts to feel like we lose this freedom that declarativeness gave us.
The opposite is the centralized state - a data structure containing the whole state of all application. Every state-depending component could connect to the particular part of the state in order to rerender when the state changes. This is a technique used e.g. by Flux, but also can be gained by a laser-focused tools.
Here comes Baobab, a tool to build and manage a tree data structure, preferably a huge one (which explains the name). Such a tree can be used as an external, centralized state of your React application. It fits into React so much, that there is already an integration with React - baobab-react package.
Let’s take a quick tour of Baobab combined with React.
Basics
First of all, you need to create a tree object using new Baobab()
constructor. The first argument it takes is a plain object, from which a tree will be created.
The second argument is the options, but they are not necessary. We will leave it for now, having a tree configurated by default.
Let’s say that tree_data
object represents the state of our app. We expect these data to be updated in the future.
var Baobab = require('baobab'),
var tree_data = {
guitarists: [
{
firstname: 'Angus',
lastname: 'Young'
},
{
firstname: 'Jimmy',
lastname: 'Page'
}
]
};
var tree = new Baobab(tree_data);
Cursors
To traverse such a tree you need a cursor. This is basicly a pointer to a subtree. To define a cursor you need to pass some kind of an address:
var maestrosCursor = tree.select('guitarists');
maestrosCursor.get();
// [{"firstname":"Angus","lastname":"Young"},{"firstname":"Jimmy","lastname":"Page"}]
var angusCursor = tree.select('guitarists', 0);
angusCursor.get();
// {"firstname":"Angus","lastname":"Young"}
You can do numerous things with cursors, read about them in Baobab readme.
Combine Baobab with React
baobab-react
comes with the four ways to plug React components into the tree:
- Mixins
- Higher Order Components
- Wrapper Components
- ES7 Decorators (experimental)
Every manner requires some code in two kinds of components - the root of the app, and the branches.
In this post, we will focus on mixins. Are you not familiar with mixins? Basically, they are the snippets of code that can be shared by many components. In case of Baobab, we can add a mixin to gain special rights to the tree.
As mentioned before, there are two kinds of Baobab mixins:
- for the Root component
- for the Branch component
Mixin for the Root component
The Root component is a component that has access to the entire tree. Likely it is the outermost component of your app.
All you have to do is:
- add
rootMixin
to the list of the component’s mixins. - pass a
tree
property to it. This should be a Baobab tree object, otherwise you will get an error.
var React = require('react'),
rootMixin = require('baobab-react/mixins').root
var Application = React.createClass({
mixins: [rootMixin],
render: function() {
return (
<div>
<h1>Hi, here is your Application</h1>
<List />
</div>
);
}
});
React.render(<Application tree={tree} />, mountNode);
Mixin for Branch components
Whenever a nested component needs to refer to the tree, you should add a branch component mixin. Let’s show it on the example of the List
component rendered inside of the above Application
component.
var branchMixin = require('baobab-react/mixins').branch;
var List = React.createClass({
mixins: [branchMixin],
cursors: {
people: ['guitarists']
},
renderItem: function(person, index) {
return (
<Item id={index} key={index} />
);
},
render: function() {
return (
<div>
Here is the list:
<ul>
{this.state.people.map(this.renderItem)}
</ul>
</div>
);
}
});
The component has branchMixin
on its mixins list, and additionally a cursors
value. This object defines what cursors has the component; i.e. which subtrees the components can look at.
The cursors replace the state. Basically - you don’t explicitly declare the state of the component. Instead, you declare the list of cursors that refer to the subtree of the external state. Every cursor creates a value that your component can access just like it was a variable in the state - this.state.<cursorName>
. It also has the most important property - whenever it changes, it triggers rerender of the component.
This means that a cursor declared like this:
cursors: {
people: ['guitarists']
},
converts to such a variable:
this.state.people
So you can still read from the state in the same way like without Baobab.
Here is the code of the Item
component rendered inside the List
:
var Item = React.createClass({
mixins: [branchMixin],
cursors: {
people: ['guitarists']
},
parseId: function() {
return parseInt(this.props.id);
},
parsePerson: function() {
var id = this.parseId();
return this.state.people[id];
},
delete: function() {
var id = this.parseId();
var guitaristsCursor = this.cursors.people;
guitaristsCursor.splice([id, 1]);
},
render: function() {
var person = this.parsePerson();
return (
<li>
<button onClick={this.delete}>Delete</button> {person.firstname} {person.lastname}
</li>
);
}
});
Note that you can still access to cursors themselves, not the subtree they point to, using this.cursors
reference. This is the way to modify a Baobab tree (in this case - delete a person by calling a splice).
Modify Baobab even more
You should allow your users to add their favourite guitarists. Consider a simple component composed of two input
elements (to type first- and lastname) and a button
(to add a guitarist to the glorious list).
var AddGuitaristForm = React.createClass({
mixins: [branchMixin],
cursors: {
people: ['guitarists']
},
getInitialState: function() {
return {
firstname: "",
lastname: ""
};
},
handleClick: function() {
var newValue = {
firstname: this.state.firstname,
lastname: this.state.lastname
};
var guitaristsCursor = this.cursors.people;
guitaristsCursor.push(newValue);
},
handleFirstNameChange: function(event) {
this.setState({firstname: event.target.value});
},
handleLastNameChange: function(event) {
this.setState({lastname: event.target.value});
},
render: function() {
return (
<div>
Add a maestro:
<input type="text" value={this.state.firstname} onChange={this.handleFirstNameChange} />
<input type="text" value={this.state.lastname} onChange={this.handleLastNameChange} />
<button onClick={this.handleClick}>Add</button>
</div>
);
}
});
Let’s see once again the key method here.
handleClick: function() {
var newValue = {
firstname: this.state.firstname,
lastname: this.state.lastname
};
var guitaristsCursor = this.cursors.people;
guitaristsCursor.push(newValue);
},
The components see such an event as updating the state and, of course, rerender (if needed). In this case the whole List
component rerenders, in particular - adds a new Item
component. There are no differences in the components’ behaviour. It’s truly the state, only defined in the other place.
What are the benefits?
- You don’t need to pass props and callbacks through all application. Although there is a cost you have to pay - defining the cursors.
- The application state lives in one place.
- Finally, the coolest feature about Baobab. Whenever a tree is updated (no matter if by the components, or external world), the components connected to updated subtree become rerendered. Just like it was their pure, internal state.
var addGuitaristByExternalWorld = function() {
var guitaristsCursor = tree.select('guitarists');
var newGuitarist = {
firstname: "Eric",
lastname: "Clapton"
}; // Clapton is the coolest, you can't forget about him
guitaristsCursor.push(newGuitarist);
};
setInterval(addGuitaristByExternalWorld, 10000);
Such a code (not being React-ish at all) causes extending the guitarists
array by a new person every 10 seconds. The List
component sees the change and rerender.
Also, Baobab comes with some cool, unused so far features, such as:
- The Baobab tree can be immutable. You certainly remember the pros of it from our previous post about immutability. If not, you can read more.
- Since the Baobab tree is persistent, you can have undo operation for free. See the next chapter.
Undo
Baobab’s cursors have built-in undo
method which restores the subtree to the previous state. To prepare a cursor to undo actions, you have to call startRecording
method on it (with an argument being the number of steps to remember). Since we already know how to access to the cursors, the undo
action is trivial - it simply delegates to this.cursors.people.undo
.
Note that cursor.undo
throws an error when there is nothing to undo; i.e. the cursor has no history. In order not to cause that error, we specify a disabled
property of the button, when it would undo nothing.
var UndoButton = React.createClass({
mixins: [branchMixin],
cursors: {
people: ['guitarists']
},
componentWillMount: function() {
var guitaristsCursor = this.cursors.people;
guitaristsCursor.startRecording(10);
},
handleClick: function() {
this.cursors.people.undo();
},
render: function() {
var disabled = !this.cursors.people.hasHistory();
return (
<button onClick={this.handleClick} disabled={disabled}>Undo</button>
);
}
});
Although this doesn’t work yet. Why? The Baobab tree works asynchronously by default. This means that every change (like deleting a person or adding one) does not appear immediately, but rather is queued and will be launched as soon as possible. Therefore, the rerender may happen before the changes are visible.
To fix this, you can simply specify an asynchronous
option when building a tree:
var options = {
asynchronous: false
};
var tree = new Baobab(tree_data, options);
Summary
That was Baobab, a cool way to extract the tree from the states of your React components. I hope you found it interesting and useful - however, the benefits are more visible when it comes to scale.
Remember - no framework doesn’t mean no architecture. React is not a framework, but it doesn’t mean that you should do full-yolo-freestyle in the code. It means rather that you should constraint yourself and build your own structures. Such as the external state.