Baobab is a JavaScript data tree supporting cursors and enabling developers to easily navigate and monitor nested data.
It is mainly inspired by functional zippers such as Clojure's ones and by Om's cursors.
It can be paired with React easily through mixins to provide a centralized model holding your application's state.
For a concise introduction about the library and how it can be used by a React/Flux application, you can head toward @christianalfoni's article on the subject.
var Baobab = require('baobab');
var tree = new Baobab({
palette: {
colors: ['yellow', 'purple'],
name: 'Glorious colors'
}
});
var colorsCursor = tree.select('palette', 'colors');
colorsCursor.on('update', function() {
console.log('Selected colors have updated:', colorsCursor.get());
});
colorsCursor.push('orange');
If you want to use Baobab with node.js or browserify, you can use npm.
npm install baobab
# Or for the latest dev version
npm install git+https://github.com/Yomguithereal/baobab.git
If you want to use it in the browser, just include the minified script located here.
<script src="baobab.min.js"></script>
Or install with bower:
bower install baobab
Creating a baobab is as simple as instantiating it with an initial data set (note that only objects or array should be given).
var Baobab = require('baobab');
var tree = new Baobab({hello: 'world'});
// Retrieving data from your tree
tree.get();
>>> {hello: 'world'}
Then you can create cursors to easily access nested data in your tree and be able to listen to changes concerning the part of the tree you selected.
// Considering the following tree
var tree = new Baobab({
palette: {
name: 'fancy',
colors: ['blue', 'yellow', 'green']
}
});
// Creating a cursor on the palette
var paletteCursor = tree.select('palette');
paletteCursor.get();
>>> {name: 'fancy', colors: ['blue', 'yellow', 'green']}
// Creating a cursor on the palette's colors
var colorsCursor = tree.select('palette', 'colors');
colorsCursor.get();
>>> ['blue', 'yellow', 'green']
// Creating a cursor on the palette's third color
var thirdColorCursor = tree.select('palette', 'colors', 2);
thirdColorCursor.get();
>>> 'green'
// Note you can also perform subselections if needed
var colorCursor = paletteCursor.select('colors');
A baobab tree can obviously be updated. However, one has to understand that he won't do it, at least by default, synchronously.
Rather, the tree will stack and merge every update order you give it and will only commit them at the next frame or next tick in node.
This enables the tree to perform efficient mutations and to be able to notify any relevant cursors that the data they are watching over has changed.
Setting a key
tree.set('hello', 'world');
Unsetting a key
tree.unset('hello');
Replacing data at cursor
cursor.edit({hello: 'world'});
Setting a key
cursor.set('hello', 'world');
Removing data at cursor
cursor.remove();
Unsetting a key
cursor.unset('hello');
Pushing values
Obviously this will fail if target data is not an array.
cursor.push('purple');
cursor.push(['purple', 'orange']);
Unshifting values
Obviously this will fail if target data is not an array.
cursor.unshift('purple');
cursor.unshift(['purple', 'orange']);
Applying a function
cursor.apply(function(currentData) {
return currentData + 1;
});
Shallowly merging objects
cursor.merge({hello: 'world'});
Whenever an update is committed, events are fired to notify relevant parts of the tree that data was changed so that bound elements, React components, for instance, can update.
Note however that only relevant cursors will be notified of data change.
Events can be bound to either the tree or cursors using the on
method.
Example
// Considering the following tree
var tree = new Baobab({
users: {
john: {
firstname: 'John',
lastname: 'Silver'
},
jack: {
firstname: 'Jack',
lastname: 'Gold'
}
}
});
// And the following cursors
var usersCursor = tree.select('users'),
johnCursor = usersCursor.select('john'),
jackCursor = usersCursor.select('jack');
// If we update both users
johnCursor.set('firstname', 'John the third');
jackCursor.set('firstname', 'Jack the second');
// Every cursor above will be notified of the update
// But if we update only john
johnCursor.set('firstname', 'John the third');
// Only the users and john cursors will be notified
update
Will fire if the tree is updated.
tree.on('update', fn);
invalid
Will fire if a data-validation specification was passed at instance and if new data does not abide by those specifications. For more information about this, see the data validation part of the documentation.
tree.on('invalid', fn);
update
Will fire if data watched by cursor has updated.
cursor.on('update', fn);
irrelevant
Will fire if the cursor has become irrelevant and does not watch over any data anymore.
cursor.on('irrelevant', fn);
relevant
Will fire if the cursor is irrelevant but becomes relevant again.
cursor.on('relevant', fn);
For more information concerning Baobab's event emitting, see the emmett library.
A baobab tree can easily be used as a UI model keeping the whole application state.
It is then really simple to bind this centralized model to React components by using the library's built-in mixins. Those will naturally bind components to one or more cursors watching over parts of the main state so they can update only when relevant data has been changed.
This basically makes the shouldComponentUpdate
method useless in most of cases and ensures that your components will only re-render if they need to because of data changes.
You can bind a React component to a Baobab tree and register one or more cursors. Cursors registered to a component will have their contents bound to the this.state.cursor
(or this.state.cursors
if registering multiple cursors).
Registering a cursor is as simple as defining a path.
var tree = new Baobab({
users: ['John', 'Jack'],
information: {
title: 'My fancy App'
}
});
var UserList = React.createClass({
mixins: [tree.mixin],
cursor: ['users'],
render: function() {
// Cursor data is then available either through:
var data = this.cursor.get();
// Or
var data = this.state.cursor;
return <ul>{data.map(function(username) {
return <li>{username}</li>;
})}</ul>;
}
});
You can also use a function returning a Cursor. This allows the component to be bound to a cursor passed through this.props
.
var tree = new Baobab({
users: ['John', 'Jack'],
information: {
title: 'My fancy App'
}
});
var UserGroupList = React.createClass({
mixins: [tree.mixin],
cursor: function(){
return this.props.usersCursor;
},
render: function() {
return <ul>{this.cursor.get().map(function(username) {
return <li>{username}</li>;
})}</ul>;
}
}
var users = tree.select('users');
React.render(<UserGroupList usersCursor={users}/>, document.getElementById('mount-point'));
Similarly, to bind several Cursors to a component, you can return a list of paths to cursors
.
var tree = new Baobab({
users: ['John', 'Jack'],
information: {
title: 'My fancy App'
}
});
var UserList = React.createClass({
mixins: [tree.mixin],
cursors: [['users'], ['information', 'title']],
render: function() {
return (
<div>
<h1>{this.cursors[1].get()}</h1>
<ul>{this.cursors[0].get().map(function(name) {
return <li>{name}</li>;
})}</ul>
</div>
);
}
});
To access Cursors easily you can return a Cursor "map".
var tree = new Baobab({
users: ['John', 'Jack'],
information: {
title: 'My fancy App'
}
});
var UserList = React.createClass({
mixins: [tree.mixin],
cursors: {
users: ['users'],
title: ['information', 'title']
},
render: function() {
return (
<div>
<h1>{this.cursors.title.get()}</h1>
<ul>{this.cursors.users.get().map(function(name) {
return <li>{name}</li>;
})}</ul>
</div>
);
}
});
With a function:
var tree = new Baobab({
users: ['John', 'Jack'],
information: {
title: 'My fancy App'
}
});
var UserList = React.createClass({
mixins: [tree.mixin],
cursors: function() {
return {
users: this.props.usersCursor,
title: ['information', 'title']
}
},
render: function() {
return (
<div>
<h1>{this.cursors.title.get()}</h1>
<ul>{this.cursors.users.get().map(function(name) {
return <li>{name}</li>;
})}</ul>
</div>
);
}
});
var users = tree.select('users');
React.render(<UserList usersCursor={users}/>, document.getElementById('mount-point'));
If you ever need to, know that they are many ways to select and retrieve data within a baobab.
var tree = new Baobab({
palette: {
name: 'fancy',
colors: ['blue', 'yellow', 'green'],
items: [{id: 'one', value: 'Hey'}, {id: 'two', value: 'Ho'}]
}
});
// Selecting
var colorsCursor = tree.select('palette', 'colors');
var colorsCursor = tree.select(['palette', 'colors']);
var colorsCursor = tree.select('palette').select('colors');
// Retrieving data
colorsCursor.get(1)
>>> 'yellow'
paletteCursor.get('colors', 2)
>>> 'green'
tree.get('palette', 'colors');
tree.get(['palette', 'colors']);
>>> ['blue', 'yellow', 'green']
// Retrieving or selecting data by passing a function in the path
var complexCursor = tree.select('palette', 'colors', function(color) {
return color === 'green';
});
tree.get('palette', 'colors', function(color) {
return color === 'green';
});
>>> 'green'
// Retrieving or selecting data by passing a descriptor object in the path
var complexCursor = tree.select('items', {id: 'one'}, 'value');
tree.get('items', {id: 'one'}, 'value');
>>> 'Hey'
Going up in the tree
var tree = new Baobab({first: {second: 'yeah'}})
secondCursor = tree.select('first', 'second');
var firstCursor = secondCursor.up();
Going left/right/down in lists
var tree = new Baobab({
list: [[1, 2], [3, 4]],
longList: ['one', 'two', 'three', 'four']
});
var listCursor = tree.select('list'),
twoCursor = tree.select('longList', 1);
listCursor.down().right().get();
>>> [3, 4]
listCursor.select(1).down().left().get();
>>> 3
twoCursor.leftmost().get();
>>> 'one'
twoCursor.rightmost().get();
>>> 'four'
Getting root cursor
var tree = new Baobab({first: {second: 'yeah'}}),
cursor = tree.select('first');
var rootCursor = tree.root();
// or
var rootCursor = cursor.root();
Check information about the cursor's location in the tree
cursor.isRoot();
cursor.isBranch();
cursor.isLeaf();
You can pass those options at instantiation.
var baobab = new Baobab(
// Initial data
{
palette: {
name: 'fancy',
colors: ['blue', 'green']
}
},
// Options
{
maxHistory: 5,
clone: true
}
)
- autoCommit boolean [
true
]: should the tree auto commit updates or should it let the user do so through thecommit
method? - asynchronous boolean [
true
]: should the tree delay the update to the next frame or fire them synchronously? - clone boolean [
false
]: by default, the tree will give access to references. Set totrue
to clone data when retrieving it from the tree if you feel paranoid and know you might mutate the references by accident or need a cloned object to handle. - cloningFunction function: the library's cloning method is minimalist on purpose and won't cover edgy cases. You remain free to pass your own more complex cloning function to the tree if needed.
- cursorSingletons boolean [
true
]: by default, a baobab tree stashes the created cursor so only one would be created by path. You can override this behaviour by settingcursorSingletons
tofalse
. - maxHistory number [
0
]: max number of records the tree is allowed to store within its internal history. - mixins array: optional mixins to merge with baobab's ones. Recommending the pure render one from react.
- shiftReferences boolean [
false
]: tell the tree to shift references of the objects it updates so that functions performing shallow comparisons (such as the one used by thePureRenderMixin
, for instance), can assess that data changed. - typology Typology|object: a custom typology to be used to validate the tree's data.
- validate object: a typology schema ensuring the tree's data is valid.
A baobab tree, given you instantiate it with the correct option, is able to record n of its passed states so you can go back in time whenever you want.
Example
var baobab = new Baobab({name: 'Maria'}, {maxHistory: 1});
baobab.set('name', 'Isabella');
// On next frame, when update has been committed
baobab.get('name')
>>> 'Isabella'
baobab.undo();
baobab.get('name')
>>> 'Maria'
Related Methods
// Check whether our tree hold records
baobab.hasHistory();
>>> true
// Retrieving history records
baobab.getHistory();
If you ever need to specify complex updates without resetting the whole subtree you are acting on, for readability or performance reasons, you remain free to use Baobab's internal update specifications.
Those are widely inspired by React's immutable helpers, themselves inspired by MongoDB's ones and can be used through tree.update
and cursor.update
.
Specifications
Those specifications are described by a JavaScript object that follows the nested structure you are trying to update and applying dollar-prefixed commands at leaf level.
The available commands are the following and are basically the same as the cursor's updating methods:
$set
$apply
$chain
$push
$unshift
$merge
$unset
Example
var tree = new Baobab({
users: {
john: {
firstname: 'John',
lastname: 'Silver'
},
jack: {
firstname: 'Jack',
lastname: 'Gold'
}
}
});
// From tree
tree.update({
users: {
john: {
firstname: {
$set: 'John the 3rd'
}
},
jack: {
firstname: {
$apply: function(firstname) {
return firstname + ' the 2nd';
}
}
}
}
});
// From cursor
var cursor = tree.select('users', 'john');
cursor.update({
firstname: {
$set: 'Little Johnsie'
}
})
Because updates will be committed later, update orders are merged when given and the new order will sometimes override older ones, especially if you set the same key twice to different values.
This is problematic when what you want is to increment a counter for instance. In those cases, you need to chain functions that will be assembled through composition when the update orders are merged.
var inc = function(i) {
return i + 1;
};
// If cursor.get() >>> 1
cursor.apply(inc);
cursor.apply(inc);
// will produce 2, while
cursor.chain(inc);
cursor.chain(inc);
// will produce 3
At times, you might want to listen to updates concerning a logical combination of cursors. For instance, you might want to know when two cursors both updated or when either one or the other did.
You can build cursor combination likewise:
// Simple "or" combination
var combination = cursor1.or(cursor2);
// Simple "and" combination
var combination = cursor1.and(cursor2);
// Complex combination
var combination = cursor1.or(cursor2).or(cursor3).and(cursor4);
// Listening to events
combination.on('update', handler);
// Releasing a combination to avoid leaks
combination.release();
Given you pass the correct parameters, a baobab tree is able to check whether its data is valid or not against the supplied specification.
This specification must be written in the typology library's style.
Example
var baobab = new Baobab(
// Initial state
{
hello: 'world',
colors: ['yellow', 'blue'],
counters: {
users: 3,
groups: 1
}
},
// Parameters
{
validate: {
hello: '?string',
colors: ['string'],
counters: {
users: 'number',
groups: 'number'
}
}
}
);
// If one updates the tree and does not respect the validation specification
baobab.set('hello', 42);
// Then the tree will fire an 'invalid' event containing a list of errors
baobab.on('invalid', function(e) {
console.log(e.data.errors);
});
Controlled input state
If you need to store a react controlled input's state into a baobab tree, remember you have to commit changes synchronously through the commit
method if you don't want to observe nasty cursor jumps.
var tree = new Boabab({inputValue: null});
var Input = React.createClass({
mixins: [tree.mixin],
cursor: ['inputValue'],
onChange: function(e) {
var newValue = e.target.value;
// If one edits the tree normally, i.e. asynchronously, the cursor will hop
this.cursor.edit(newValue);
// One has to commit synchronously the update for the input to work correctly
this.cursor.edit(newValue);
this.tree.commit();
},
render: function() {
return <input onChange={this.onChange} value={this.cursor.get()} />;
}
});
Immutable behaviour
TL;DR: Don't mutate things in your baobab tree. Let the tree handle its own mutations.
For performance and size reasons baobab does not (yet?) use an immutable data structure. However, because it aims at producing a one-way data flow for your application state (like React would at component level), it must be used like an immutable data structure.
For this reason, don't be surprised if you mutate things and break your tree.
// This is bad:
var users = tree.get('users');
users[0].name = 'Jonathan';
// This is also bad:
var o = {hello: 'world'};
tree.set('key', o);
o.hello = 'other world';
Contributions are obviously welcome. This project is nothing but experimental and I would cherish some feedback and advice about the library.
Be sure to add unit tests if relevant and pass them all before submitting your pull request.
# Installing the dev environment
git clone [email protected]:Yomguithereal/baobab.git
cd baobab
npm install
# Running the tests
npm test
# Linting, building
npm run lint
npm run build
MIT