JavaScript & TypeScript persistent and optionally immutable data tree with cursors.

Related tags

baobab
Overview

Build Status

Baobab

Baobab is a JavaScript & TypeScript persistent and immutable (at least by default) data tree supporting cursors and enabling developers to easily navigate and monitor nested data through events.

It is mainly inspired by functional zippers (such as Clojure's ones) and by Om's cursors.

It aims at providing a centralized model holding an application's state and can be paired with React easily through mixins, higher order components, wrapper components or decorators (available there).

Fun fact: A Baobab, or Adansonia digitata, is a very big and magnificent African tree.

Summary

Example

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.push('orange');

Installation

If you want to use Baobab with node.js or browserify/webpack etc., you can use npm.

npm install baobab

# Or if you need 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.

Note that the library comes along with its own declaration files so you can use it comfortably with TypeScript also.

<script src="baobab.min.js"></script>

Or install with bower:

bower install baobab

The library (as a standalone) currently weighs ~8kb gzipped.

Usage

Basics

Instantiation

Creating a tree is as simple as instantiating Baobab with an initial data set.

var Baobab = require('baobab');

var tree = new Baobab({hello: 'world'});

// Retrieving data from your tree
tree.get();
>>> {hello: 'world'}

Cursors

Then you can create cursors to easily access nested data in your tree and 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 that you can also perform subselections if needed
var colorCursor = paletteCursor.select('colors');

Updates

A baobab tree can obviously be updated. However, one has to understand that, even if you can write the tree synchronously, update events won't be, at least by default, fired until next frame.

If you really need to fire an update synchronously (typically if you store a form's state within your app's state, for instance), your remain free to use the tree.commit() method or tweak the tree's options to fit your needs.

Important: Note that the tree, being a persistent data structure, will shift the references of the objects it stores in order to enable immutable comparisons between one version of the state and another (this is especially useful when using strategies as such as React's pure rendering).

Example

var tree = new Baobab({hello: 'world'});

var initialState = tree.get();
tree.set('hello', 'monde');

// After asynchronous update...
assert(initialState !== tree.get());


tree/cursor.set

Replaces value at the given key or the cursor's value altogether if no value is supplied.

It will also work if you want to replace a list's item.

// Replacing the cursor's value
var newValue = cursor.set(newValue);

// Setting a precise key
var newValue = cursor.set('key', newValue);

// Setting a nested key
var newValue = cursor.set(['one', 'two'], newValue);
var newValue = cursor.select('one', 'two').set(newValue);
var newValue = cursor.select('one').set('two', newValue);
tree/cursor.unset

Unsets the given key.

It will also work if you want to delete a list's item.

// Removing data at cursor
cursor.unset();

// Removing a precise key
cursor.unset('key');

// Removing a nested key
cursor.unset(['one', 'two']);
tree/cursor.push

Pushes a value into the selected list. This will of course fail if the selected node is not a list.

// Pushing a value
var newList = cursor.push(newValue);

// Pushing a value in the list at key
var newList = cursor.push('key', newValue);

// Pushing into a nested path
var newList = cursor.push(['one', 'two'], newValue);
var newList = cursor.select('one', 'two').push(newValue);
var newList = cursor.select('one').push('two', 'world');
tree/cursor.unshift

Unshifts a value into the selected list. This will of course fail if the selected node is not a list.

// Unshifting a value
var newList = cursor.unshift(newValue);

// Unshifting a value in the list at key
var newList = cursor.unshift('key', newValue);

// Unshifting into a nested path
var newList = cursor.unshift(['one', 'two'], newValue);
var newList = cursor.select('one', 'two').unshift(newValue);
var newList = cursor.select('one').unshift('two', newValue);
tree/cursor.concat

Concatenates a list into the selected list. This will of course fail if the selected node is not a list.

// Concatenating a list
var newList = cursor.concat(list);

// Concatenating a list in the list at key
var newList = cursor.concat('key', list);

// Concatenating into a nested path
var newList = cursor.concat(['one', 'two'], list);
var newList = cursor.select('one', 'two').concat(list);
var newList = cursor.select('one').concat('two', list);
tree/cursor.pop

Removes the last item of the selected list. This will of course fail if the selected node is not a list.

// Popping the list
var newList = cursor.pop();

// Popping the list at key
var newList = cursor.pop('key');

// Popping list at path
var newList = cursor.pop(['one', 'two']);
var newList = cursor.select('one', 'two').pop();
var newList = cursor.select('one').pop('two');
tree/cursor.shift

Removes the first item of the selected list. This will of course fail if the selected node is not a list.

// Shifting the list
var newList = cursor.shift();

// Shifting the list at key
var newList = cursor.shift('key');

// Shifting list at path
var newList = cursor.shift(['one', 'two']);
var newList = cursor.select('one', 'two').shift();
var newList = cursor.select('one').shift('two');
tree/cursor.splice

Splices the selected list. This will of course fail if the selected node is not a list.

The splice specifications works the same as for Array.prototype.splice. There is one exception though: Per specification, splice deletes no values if the deleteCount argument is not parseable as a number. The splice implementation of Baobab instead throws an error, if the given deleteCount argument could not be parsed.

// Splicing the list
var newList = cursor.splice([1, 1]);

// Omitting the deleteCount argument makes splice delete no elements.
var newList = cursor.splice([1]);

// Inserting an item etc.
var newList = cursor.splice([1, 0, 'newItem']);
var newList = cursor.splice([1, 0, 'newItem1', 'newItem2']);

// Splicing the list at key
var newList = cursor.splice('key', [1, 1]);

// Splicing list at path
var newList = cursor.splice(['one', 'two'], [1, 1]);
var newList = cursor.select('one', 'two').splice([1, 1]);
var newList = cursor.select('one').splice('two', [1, 1]);
tree/cursor.apply

Applies the given function to the selected value.

var inc = function(nb) {
  return nb + 1;
};

// Applying the function
var newList = cursor.apply(inc);

// Applying the function at key
var newList = cursor.apply('key', inc);

// Applying the function at path
var newList = cursor.apply(['one', 'two'], inc);
var newList = cursor.select('one', 'two').apply(inc);
var newList = cursor.select('one').apply('two', inc);
tree/cursor.merge

Shallow merges the selected object with another one. This will of course fail if the selected node is not an object.

// Merging
var newList = cursor.merge({name: 'John'});

// Merging at key
var newList = cursor.merge('key', {name: 'John'});

// Merging at path
var newList = cursor.merge(['one', 'two'], {name: 'John'});
var newList = cursor.select('one', 'two').merge({name: 'John'});
var newList = cursor.select('one').merge('two', {name: 'John'});
tree/cursor.deepMerge

Deep merges the selected object with another one. This will of course fail if the selected node is not an object.

// Merging
var newList = cursor.deepMerge({user: {name: 'John'}});

// Merging at key
var newList = cursor.deepMerge('key', {user: {name: 'John'}});

// Merging at path
var newList = cursor.deepMerge(['one', 'two'], {user: {name: 'John'}});
var newList = cursor.select('one', 'two').deepMerge({user: {name: 'John'}});
var newList = cursor.select('one').deepMerge('two', {user: {name: 'John'}});

Events

Whenever an update is committed, events are fired to notify relevant parts of the tree that data was changed so that bound elements, UI components, for instance, may update.

Note however that only relevant cursors will be notified of a 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
Tree level

update

Will fire if the tree is updated (this concerns the asynchronous updates of the tree).

tree.on('update', function(e) {
  var eventData = e.data;

  console.log('Current data:', eventData.currentData);
  console.log('Previous data:', eventData.previousData);
  console.log('Transaction details:', eventData.transaction);
  console.log('Affected paths', eventData.paths);
});

write

Will fire whenever the tree is written (synchronously, unlike the update event).

tree.on('write', function(e) {
  console.log('Affected path:', e.data.path);
});

invalid

Will fire if the validate function (see options) returned an error for the current update.

tree.on('invalid', function(e) {
  console.log('Error:', e.data.error);
});

get

Will fire whenever data is accessed in the tree.

tree.on('get', function(e) {
  console.log('Path:', e.data.path);
  console.log('Solved path:', e.data.solvedPath);
  console.log('Target data:', e.data.data);
});

select

Will fire whenever a path is selected in the tree.

tree.on('select', function(e) {
  console.log('Path:', e.data.path);
  console.log('Resultant cursor:', e.data.cursor);
});
Cursor level

update

Will fire if data watched over by the cursor has updated.

cursor.on('update', function(e) {
  var eventData = e.data;
  console.log('Current data:', eventData.currentData);
  console.log('Previous data:', eventData.previousData);
});
N.B.

For more information concerning Baobab's event emitting, see the emmett library.

Advanced

Polymorphisms

If you ever need to, know that there are many ways to select and retrieve data within a baobab.

var tree = new Baobab({
  palette: {
    name: 'fancy',
    colors: ['blue', 'yellow', 'green'],
    currentColor: 1,
    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');

var paletteCursor = tree.select('palette');

// 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('palette', 'items', {id: 'one'}, 'value');
tree.get('palette', 'items', {id: 'one'}, 'value');
>>> 'Hey'

// Creating a blank tree
var blankTree = new Baobab();

Note: when using a function or a descriptor object in a path, you are not filtering but rather selecting the first matching element. (It's actually the same as using something like lodash's _.find).

Computed data or "Monkey Business"

For convenience, Baobab allows you to store computed data within the tree.

It does so by letting you create "monkeys" that you should really consider as dynamic nodes within your tree (v1 users: "monkeys" are merely the evolution of "facets").

As such, while monkeys represent reduction of the current state (a filtered list used by multiple components throughout your app, for instance), they do have a physical existence within the tree.

This means that you can add / modify / move / remove monkeys from the tree at runtime and place them wherever you want.

The reason why computed data now sits within the tree itself is so that components don't need to know from which kind of data, static or computed, they must draw their dependencies and so that read/select API might stay the same across the whole library.

Example

var monkey = Baobab.monkey;
// Or if you hate similes and fancy naming
var dynamicNode = Baobab.dynamicNode;

// Declarative definition syntax
var tree = new Baobab({
  user: {
    name: 'John',
    surname: 'Smith',
    fullname: monkey({
      cursors: {
        name: ['user', 'name'],
        surname: ['user', 'surname']
      },
      get: function(data) {
        return data.name + ' ' + data.surname;
      }
    })
  },
  data: {
    messages: [
      {from: 'John', txt: 'Hey'},
      {from: 'Jack', txt: 'Ho'}
    ],
    fromJohn: monkey({
      cursors: {
        messages: ['data', 'messages'],
      },
      get: function(data) {
        return data.messages.filter(function(m) {
          return m.from === 'John';
        });
      }
    })
  }
});

// Alternate shorthand definition syntax
var tree = new Baobab({
  user: {
    name: 'John',
    surname: 'Smith',
    fullname: monkey(
      ['user', 'name'],
      ['user', 'surname'],
      function(name, surname) {
        return name + ' ' + surname;
      }
    )
  },
  data: {
    messages: [
      {from: 'John', txt: 'Hey'},
      {from: 'Jack', txt: 'Ho'}
    ],
    fromJohn: monkey(
      ['data', 'messages'],
      function(messages) {
        return messages.filter(function(m) {
          return m.from === 'John';
        });
      }
    )
  }
});

// Possibility to disable a single monkey's immutability
var tree = new Baobab({
  data: {
    users: ['Jack', 'John'],
    onlyJack: monkey({
      cursors: {
        users: ['data', 'users'],
        get: function(data) {
          return data.users.filter(function(user) {
            return user === 'Jack';
          });
        },
        options: {
          immutable: false
        }
      }
    }),

    // Using the shorthand
    onlyJohn: monkey(
      ['data', 'users'],
      function(users) {
        return users.filter(function(user) {
          return user === 'John';
        });
      },
      {immutable: false}
    )
  }
});

// Finally, know that you can use relative paths for convenience
var tree = new Baobab({
  data: {
    user: {
      name: 'John',
      surname: 'Smith',
      fullname: monkey(
        ['.', 'name'],
        ['.', 'surname'],
        function(name, surname) {
          return name + ' ' + surname;
        }
      ),
      evenMoreNested: {
        fullname: monkey(
          ['..', 'name'],
          ['..', 'surname'],
          function(name, surname) {
            return name + ' ' + surname;
          }
        )
      }
    }
  }
});

// You can then access or select data naturally
tree.get('user', 'fullname');
>>> 'John Smith'

tree.get('data', 'fromJohn');
>>> [{from: 'John', txt: 'Hey'}]

// You can also access/select data beneath a monkey
tree.get('data', 'fromJohn', 'txt');
>>> 'Hey'

var cursor = tree.select('data', 'fromJohn', 'txt');

// Just note that computed data node is read-only and that the tree
// will throw if you try to update a path lying beyond a computed node
tree.set(['data', 'fromJohn', 'txt'], 'Yay');
>>> Error!

// You can add / remove / modify a monkey at runtime using the same API
tree.set(['data', 'fromJack'], monkey({
  cursors: {
    messages: ['data', 'messages'],
    function(messages) {
      return messages.filter(function(m) {
        return m.from === 'Jack';
      });
    }
  }
}));

Notes

  • The dynamic nodes will of course automatically update whenever at least one of the watched paths is updated.
  • The dynamic nodes are lazy and won't actually be computed before you get them (plus they will only compute once before they need to change, so if you get the same dynamic node twice, the computation won't rerun).
  • There are cases where it is clearly overkill to rely on a dynamic node. For instance, if only a single component of your app needs to access a computed version of the central state, then compute this version into the rendering logic of said component for simplicity's sake (a React component's render function for instance). Dynamic nodes are somewhat part of an optimization scheme.
  • Know that the tree/cursor.serialize method exists would you need to retrieve data stripped of dynamic nodes from your tree.
  • For the time being, placing monkeys beneath array nodes is not allowed for performance reasons.

Specialized getters

tree/cursor.exists

Check whether a specific path exists within the tree (won't fire a get event).

// Probably true
tree.exists();

// Does the cursor points at an existing path?
cursor.exists();

// Can also take a path
tree.exists('hello');
tree.exists('hello', 'message');
tree.exists(['hello', 'message']);

tree/cursor.clone

Shallow clone the cursor's data. The method takes an optional nested path.

var tree = new Baobab({user: {name: 'John'}}),
    cursor = tree.select('user');

assert(cursor.get() !== cursor.clone());

tree/cursor.deepClone

Same as the tree/cursor.clone except that it will deep clone the data.

tree/cursor.serialize

Retrieve only raw data (therefore avoiding computed data) from the tree or a cursor.

This is useful when you want to serialize your tree into JSON, for instance.

tree.serialize();
cursor.serialize();

// Can also take a path
tree.serialize('hello');
tree.serialize('hello', 'message');
tree.serialize(['hello', 'message']);

tree.watch

Create a watcher that will fire an update event if any of the given paths is affected by a transaction.

This is useful to create modules binding a state tree to UI components.

// Considering the following tree
var tree = new Baobab({
  one: {
    name: 'John'
  },
  two: {
    surname: 'Smith'
  }
});

var watcher = tree.watch({
  name: ['one', 'name'],
  surname: ['two', 'surname']
});

watcher.on('update', function(e) {
  // One of the watched paths was updated!
});

watcher.get();
>>> {
  name: 'John',
  surname: 'Smith'
}

tree/cursor.project

Retrieve data from several parts of the tree by following the given projection:

// Considering the following tree
var tree = new Baobab({
  one: {
    name: 'John'
  },
  two: {
    surname: 'Smith'
  }
});

// Using an object projection
tree.project({
  name: ['one', 'name'],
  surname: ['two', 'surname']
});
>>> {name: 'John', surname: 'Smith'}

// Using an array projection
tree.project([
  ['one', 'name'],
  ['two', 'surname']
]);
>>> ['John', 'Smith']

Traversal

Getting root cursor

var tree = new Baobab({first: {second: 'yeah'}}),
    cursor = tree.select('first');

var rootCursor = tree.root;
// or
var rootCursor = cursor.root();

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().right().get();
>>> 4

listCursor.select(1).down().right().left().get();
>>> 3

twoCursor.leftmost().get();
>>> 'one'

twoCursor.rightmost().get();
>>> 'four'

Mapping cursors over a list node

var tree = new Baobab({list: [1, 2, 3]});

tree.select('list').map(function(cursor, i) {
  console.log(cursor.get());
});
>>> 1
>>> 2
>>> 3

Getting information about the cursor's location in the tree

cursor.isRoot();
cursor.isBranch();
cursor.isLeaf();

Options

You can pass those options at instantiation.

var baobab = new Baobab(

  // Initial data
  {
    palette: {
      name: 'fancy',
      colors: ['blue', 'green']
    }
  },

  // Options
  {
    autoCommit: false
  }
)
  • autoCommit boolean [true]: should the tree auto commit updates or should it let the user do so through the commit method?
  • asynchronous boolean [true]: should the tree delay the update to the next frame or fire them synchronously?
  • immutable boolean [true]: should the tree's data be immutable? Note that immutability is performed through Object.freeze and should be disabled in production for performance reasons.
  • lazyMonkeys boolean [true]: should the monkeys be lazy? Disable this option for easier debugging in your console (getter functions are sometimes hard to read in the console).
  • monkeyBusiness boolean [true]: should the tree support monkeys? Disabling this yields significant performance boost for large trees without monkeys.
  • persistent boolean [true]: should the tree be persistent. Know that disabling this option, while bringing a significant performance boost on heavy data, will make you lose the benefits of your tree's history and O(1) comparisons of objects.
  • pure boolean [true]: by default, on set and apply operations, the tree will check if the given value and the target node are stricly equal. If they indeed are, the tree won't update.
  • validate function: a function in charge of validating the tree whenever it updates. See below for an example of such function.
  • validationBehavior string [rollback]: validation behavior of the tree. If rollback, the tree won't apply the current update and fire an invalid event while notify will only emit the event and let the tree enter the invalid state anyway.

Validation function

function validationFunction(previousState, newState, affectedPaths) {
  // Perform validation here and return an error if
  // the tree is invalid
  if (!valid)
    return new Error('Invalid tree because of reasons.');
}

var tree = new Baobab({...}, {validate: validationFunction});

History

Baobab lets you record the successive states of any cursor so you can seamlessly implement undo/redo features.

Example

// Synchronous tree so that examples are simpler
var baobab = new Baobab({colors: ['blue']}, {asynchronous: false}),
    cursor = baobab.select('colors');

// Starting to record state, with 10 records maximum
cursor.startRecording(10);

cursor.push('yellow');
cursor.push('purple');
cursor.push('orange');

cursor.get();
>>> ['blue', 'yellow', 'purple', 'orange']

cursor.undo();
cursor.get();
>>> ['blue', 'yellow', 'purple']

cursor.undo(2);
cursor.get();
>>> ['blue']

Starting recording

If you do not provide a maximum number of records, will record everything without any limit.

cursor.startRecording(maxNbOfRecords);

Stoping recording

cursor.stopRecording();

Undoing

cursor.undo();
cursor.undo(nbOfSteps);

Clearing history

cursor.clearHistory();

Checking if the cursor has an history

cursor.hasHistory();

Retrieving the cursor's history

cursor.getHistory();

Common pitfalls

Releasing

In most complex use cases, you might need to release the manipulated objects, i.e. kill their event emitters and wipe their associated data. For example, cursors with a dynamic path (e.g. var cursor = tree.select(['items', { id: 5 }])), will always create a cursor with listeners that need to be released when you are done using the cursor.

Thus, any tree or cursor object can be cleared from memory by using the release method.

tree.release();
cursor.release();
watcher.release();

Note also that releasing a tree will consequently and automatically release every of its cursors and computed data nodes.

Philosophy

User interfaces as pure functions

User interfaces should be, as far as possible, considered as pure functions. Baobab is just a way to provide the needed arguments, i.e. the data representing your app's state, to such a function.

Considering your UIs like pure functions comes along with collateral advantages like easy undo/redo features, state storing (just save your tree in the localStorage and here you go) and easy usage in both client & server.

Only data should enter the tree

You shouldn't try to shove anything else than raw data into the tree. The tree hasn't been conceived to hold classes or fancy indexes with many circular references and cannot perform its magic on it. But, probably such magic is not desirable for those kind of abstractions anyway.

That is to say the data you insert into the tree should logically be JSON-serializable else you might be missing the point.

Migration

From v1 to v2

  • The tree is now immutable by default (but you can shunt this behavior through a dedicated option).
  • Writing to the tree is now synchronous for convenience. Updates remain asynchronous for obvious performance reasons.
  • You cannot chain update methods now since those will return the affected node's data to better tackle immutability.
  • The strange concat-like behavior of the push and unshift method was dropped in favor of the concat method.
  • Facets are now full-fledged dynamic nodes called monkeys.
  • The weird $cursor sugar has been dropped.
  • The update specifications have been dropped.

From v0.4.x to v1

A lot of changes occurred between 0.4.x and 1.0.0. Most notable changes being the following ones:

  • The tree now shift references by default.
  • React integration has improved and is now handled by baobab-react.
  • cursor.edit and cursor.remove have been replaced by cursor.set and cursor.unset single argument polymorphisms.
  • A lot of options (now unnecessary) have been dropped.
  • Validation is no longer handled by typology so you can choose you own validation system and so the library can remain lighter.
  • Some new features such as: $splice, facets and so on...

For more information, see the changelog.

Contribution

See CONTRIBUTING.md.

License

MIT

Issues
  • Facets are actually implementation leak

    Facets are actually implementation leak

    I think about this thing... Facets described as "views over data". But the same thing may be said about cursors. They are also "views over data". The difference is that one data is "static" and other is "dynamic". But this means nothing. If we have c = f(b) rule we never conclude that c has different nature than b. Derived and initial data are expressed in the same syntax and are equal for the consumers. The whole Math and Computer Science are based on that.

    Unfortunately, this is not the case with facets. Client code must be aware of this artifical separation:

    let foo = state.facets.foo;
    vs
    let foo = state.select("foo").get()
    

    or

    @branch({
      cursors: ...
      vs 
      facets: ...
    })
    

    This seems wrong to me. I shouldn't be concerned about such private details of the data in the client code. Is it "static" or "dynamic"? I don't care. I shouldn't ask. But now the client code and implicit rules about our data are coupled and we can't simply switch between a <- b and b <- a causalities. This means implementation leak.

    Unless I miss something, I propose to think about merging cursor and facet concepts into one more powerful abstraction (keeping cursor name). So cursors may be expressed in terms of cursors and static data then.

    But... saying that... I'm afraid that we actually reinvent the wheel here. This issues @christianalfoni raised: https://github.com/Yomguithereal/baobab-react/issues/44 https://github.com/Yomguithereal/baobab/issues/180 push me even more to the thought that Baobab will benefit being built over smarter abstraction(s). Event emitters are too primitive. We want to control initial states, we want to have movable parts having single app state at the same time. We want more and more complex primitives to express relations between data in facets like filters of all kinds...

    It sounds like... RxJS Observables could handle this better. Or CSP channels.

    There are attempts to bind React and Rx... with more or less luck. https://github.com/fdecampredon/rx-flux https://github.com/r3dm/thundercats

    Noone of them takes the concept of single app state, they are basically follow the Flux path having distinct Stores. But in everything else... we are moving to the same direction. Any state (including Baobab trees, of course) can be expressed in terms of temporal reduce function named scan. The difference between it and familiar reduce we used to is that this scan broadcasts every new state to the observers, not just returns one final data (because most of data sources never finish). I wonder if it's possible to just drop all that event emitter poor machinery and rebuilt everything on something more powerful and more suitable to our big big big list of requirements. Sounds scary, I know.

    I'd like to think I overcomplicate things and there is a well-defined outline of what Baobab should and shouldn't do. Somewhere. But I'm afraid I'm not.

    :camel:

    enhancement question 
    opened by ivan-kleshnin 64
  • Baobab v2 proposal

    Baobab v2 proposal

    Baobab v2 proposal

    This document is a proposal for the release of Baobab v2. Some things need to be discussed here before we can advance, and this is really the point of this issue to discuss.

    Installation

    You can install the latest published dev version of the v2 with:

    npm install [email protected]
    

    Updated baobab-react will soon follow and soon will be created baobab-deku.

    Immutability

    The tree is now immutable by default. My advice about this is to develop your apps with immutability (using `"use strict" so it would throw if you attempt to mutate) on and to disable it for production to see a welcome performance boost (I'd say ~30%).

    Syncwrite

    The tree now write synchronously only but will continue firing its update events asynchronously for obvious performance reasons.

    Update specifications & transactions

    Update specifications have been dropped in favor of a simpler spec.

    An update is now internally:

    {path: [...], type: 'set', value: 'Hey'}
    

    Furthermore, the tree's update events now expose the full detail of the transactions so you can replay them elsewhere or display debugging information etc. (@christianalfoni :-) )

    No more default limit on cursor's history

    By default, cursor's history limit is set to Infinity.

    Cursor setters

    Cursors' setters now longer return themselves. This was made so you can chain them but now that we should embrace immutability, returning the affected node seems way more pratical than having to write then get the affected node to carry on your operations.

    push/unshift & concat method

    Dropped the strange push/unshift concat-like behavior.

    Facets rework

    Following the discussion in #240, facets are now to be defined within the tree itself using keys starting with a dollar by convention.

    Example

    const tree = new Baobab({
      data: {
        messages: [
          {from: 'John', text: 'Hey'},
          {from: 'Jack', text: 'Ho'}
        ],
        $fromJohn: {
          cursors: {
            messages: ['data', 'messages'],
            get: function({messages}) {
              return messages.filter(m => m.from === 'John');
            }
          }
        }
      }
    });
    

    This way, there is only one way to select and get data in the tree and one can just create cursor on computed data and even select/get data beyond them:

    var cursor = tree.select('data', '$fromJohn', 0, 'text');
    

    Disclaimer

    The current dev version only allows to define facets at the tree's instantiation but v2 will allow you to modify/set/unset facets on the fly by using the tree's setters like you would normally.

    The only problem there is that once the facet has been define, you can't really access its definition anymore.

    Facets remain of course lazy on get and won't be computed until you really need them.

    Alternative facet definition syntax

    const tree = new Baobab({
      data: {
        messages: [
          {from: 'John', text: 'Hey'},
          {from: 'Jack', text: 'Ho'}
        ],
        $fromJohn: [
          ['data', 'messages'],
          function(messages) {
            return messages.filter(m => m.from === 'John');
          }
        ]
      }
    });
    

    tree/cursor.serialize

    Since there is now computed data sitting within the tree itself, one wouldn't want to get it when "serializing" the data, for instance when you would need to save it to the localStorage, for instance. This is exactly what the cursor.serialize method does.

    The method would return data at any cursor but carefully avoiding any computed data.

    tree/cursor.project

    Naming alert here! I did not find a better name. This method will probably used only internally but still. I'll take any better name.

    var data = cursor.project({
      messages: ['data', 'messages'],
      user: ['user']
    });
    // Will return an object with the related data
    
    var data = cursor.project([
      ['data', 'messages'],
      ['user']
    ]);
    // Will return an array with the related data
    

    tree.watch

    Used to watch a collection of path in the tree and fire updates if any of the paths is affected. (Used by baobab-react).

    Dropped $cursor

    No more silly $cursor sugar.

    Misc

    The code has been rewritten in ES6 and is now fully self-documented.

    emmett has been updated to its latest version.

    Full Provisional v2 Changelog

    • The tree is now immutable by default.
    • Cursor's setters method won't return themselves but rather the affected node now.
    • Adding cursor.concat.
    • Adding the cursor.serialize method.
    • Adding the cursor.project method.
    • Changing the way you can define computed data in the tree, aka "facets". Facets are now to be defined within the tree itself and can be accessed using the exact same API as normal data.
    • Adding an alternative facet definition syntax for convenience.
    • Dropped the syncwrite option. The tree is now writing synchronously but still emits its updates asynchronously by default.
    • Max number of records is now set to Infinity by default, meaning there is no limit.
    • Update events are now exposing the detail of each transaction so you can replay them elsewhere.
    • Fixing cursor.push/unshift behavior.
    • Dropped the $cursor helper.
    • Dropped the update specs for a simpler transaction syntax.
    • Updated emmett to 3.1.0.
    • ES6 codebase rewrite.
    • Full code self documentation.
    enhancement question 
    opened by Yomguithereal 41
  • Subsequent writes are lost

    Subsequent writes are lost

    Even with "chaining":

    // ACTIONS
    
    // This is caching wrapper for data fetching
    function loadMany(page, query) {
      // State.select("robots", "page").set(page); // lost
      // State.select("robots").set("page", page); // lost
      State.select("robots", "page").chain(() => parseInt(page)); // this write is lost as well
      // ... cache access will be here
      return fetchMany(page, query);
    }
    
    // This is real data-fetching action
    function fetchMany() {
      ...
      return Axios.get(apiURL, {params})
        .then(response => {
          ...
          State.select("robots").merge({ // merge shouldn't influence "page" but it does &_&
            loadError: false,
            total: meta.page && meta.page.total || Object.keys(models).length,
            models: models,
          });
          ...
    }
    

    As you can see even while first and second writes have zero overlapping keys, data from first write is lost. Perhaps chaining in both cases could help but writes occur in different functions and I can't bridge such dependencies. So despite so many syntax choices I can't make the simplest case working. I think this should be considered as an unexpected / bad behavior. Data shouldn't be lost so easy. Well, data shouldn't be lost at all. Can this chaining be the default behavior except maybe some really tricky cases?

    bug 
    opened by ivan-kleshnin 40
  • [v2] Facet API proposition

    [v2] Facet API proposition

    I have been discussing the new facets API with @Yomguithereal, and here are the problems with the current propositions:

    • I think it is too dangerous to prefix keys in the tree as does the current implementation, since I do not always know all the keys I am manipulating in my tree, and I cannot guarantee that there are no key starting with a $ (mostly when it comes from JSON documents).
    • @christianalfoni's solution fixes this issue, but having a function to return an object - and 99.9% of the time will do absolutely nothing else, seems a bit weird to me.

    Another idea would be to use a facets factory. This would give us something like:

    var Baobab = require('baobab');
    
    var tree = Baobab({
      user: {
        name: 'John',
        surname: 'Smith',
    
        // Facet:
        fullname: Baobab.facet({
          cursors: {
            name: ['user', 'name'],
            surname: ['user', 'surname']
          },
          get: function(data) {
            return data.name + ' ' + data.surname;
          }
        }
      })
    });
    

    What do you think about this?

    enhancement 
    opened by jacomyal 39
  • Remove methods

    Remove methods

    Related to #65.

    enhancement 
    opened by Yomguithereal 29
  • Monkeys implementation

    Monkeys implementation

    Here is how monkeys work:

    • Baobab.monkey creates a MonkeyDefinition instance that can be placed anywhere in the tree.
    • When the tree is written, data is parsed to find every MonkeyDefinition contained in it.
    • A "parallel" tree containing Monkey instances is created:
    // For the following tree:
    {
      one: {hello: 'world'},
      two: {user: 'michael', dynamic: monkey(...)}
    }
    
    // The following monkey index is created:
    {
      two: {dynamic: ...}
    }
    
    • Each monkey will then rewrite the tree's data by replacing their host node by a lazy JS getter computing the resultant data (memoized, of course).
    • If any of the deps is updated, the monkey will rewrite the lazy getter.
    • When the user write the tree, the monkeys index is updated (starting from the updated node for perf of course).
    discussion 
    opened by Yomguithereal 28
  • v2.0.0 Roadmap

    v2.0.0 Roadmap

    • State/facets convergence (#240).
    • Dropping $cursor.
    • Better code self documentation.
    • Dropping $chain.
    • Probably converge towards syncwrite.
    • immutable by default?
    enhancement 
    opened by Yomguithereal 27
  • Setting values via path array?

    Setting values via path array?

    Hi,

    I saw in the readme that you can create a cursor by passing in a path array:

    var colorsCursor = tree.select(['palette', 'colors']);
    

    So for some reason I ended up assuming that you can also set values by passing in a path array like so:

    tree.set(['palette', 'colors'], 'blue');
    

    However this doesn't seem to be the case:

    tree.set(['palette', 'colors'], 'blue');
    tree.commit();
    tree.get();
    //Object {palette,colors: "blue"}
    

    Calling set with a path array actually leads to Baobab setting the value at the path of a comma-joined string of the path array, which is quite odd.

    The workaround I'm using right now is calling select with a path array and then edit to change the value, but I feel being able to pass in a path array to set would be a more intuitive way to do this.

    Thoughts?

    Thank you for making this amazing library by the way! =)

    bug enhancement 
    opened by lewisl9029 24
  • Best practice to maintain canonical stores?

    Best practice to maintain canonical stores?

    I'm running into issues when I start to throw more advanced scenarios at Baobab. I'm hoping somebody has ideas on how to handle these scenarios. An example - let's say I have a tree like this:

    var state = new Baobab({
      projects: {
        error: null,
        loading: false,
        data: {}    
      },
    
      currentProject: {
        error: null,
        loading: false,
        data: null    
      },
    
      filteredProjects: ???
    });
    

    Ideally: projects.data holds all the projects that have been fetched in the life of the application. This is the canonical store for projects. If I change projects.data.123's name, this change should be reflected everywhere in the application.

    currentProject.data should point to or hold the data for the currently selected project. filteredProjects should point to or hold a subset of projects.data, depending on the filters the user has chosen.

    React view A has a cursor for currentProject. React view B has a cursor for filteredProjects. The problem here is that when a user updates the name of the current project, the app needs to manually update the project name in all three locations (projects.data, currentProject.data, filteredProjects.data). This manual updating can quickly grow out of control.

    I tried setting currentProject.data to a cursor that points to the project in projects.data (basically trying to treat it like a pointer), but the currentProject update event was not fired when the corresponding project in projects.data was updated.

    So my questions are:

    1. Is there a way to have a cursor point to another cursor, so that data does not need to be duplicated/denormalized (which introduces all sorts of update complexity).
    2. Is there a best practice around creating a cursor that includes a subset of the tree data? In the example above, I need a cursor that includes only some of the projects.

    Looking forward to hearing your thoughts, and please let me know if the above is confusing or needs more explanation.

    question 
    opened by marbemac 23
  • Performance of push has O(n)

    Performance of push has O(n)

    Hi @Yomguithereal

    in our application, the main baobab-managed data structure is an array. We now realized that the runtime of each push operation climbs with growing array size. In fact, the runtime seems to be O(n) as the push operation copies the whole array as can be seen here:

    https://github.com/Yomguithereal/baobab/blob/master/src/update.js#L112

    Here is a Script that pushes 100.000 items into a baobab array and plots out the runtime per 1000 push operations (the output can easily be dropped into a spreadsheet to draw a graph from it):

    https://gist.github.com/ayalpani/7d66c4f2c87641bdcdd6#file-test-js

    On my machine, the first 10.000 array items are pushed within 647 ms while items 90.000-100.000 take 11616 ms

    I understand that you are cloning the array to enable immutability. If immutable option is turned off, couldn't you use native JavaScript methods like Array.prototype.push in general? This would enable a faster runtime when possible.

    Best, Arash

    question 
    opened by ayalpani 22
  • Support structural equality for keys

    Support structural equality for keys

    Would it be possible, similar to Immutable.JS (or Java..), have support for keys based on structural equality (ie. hashCode, equals)?

    opened by pbadenski 8
  • Unify output from tree events and cursor events

    Unify output from tree events and cursor events

    Hi ! I was playing around with the library and my use-case is using baobab on this stucture :

    const test = {
        "model": {
            "name": "foo",
            "list1": [{
                    "prop1": "hey"
                },
                {
                    "prop1": "ho"
                }
            ],
            "list2": [{
                    "prop2": "bye"
                },
                {
                    "prop2": "seeya"
                }
            ]
        }
    }
    

    Let's create the tree like so const tree = new Baobab(test); If I attach my watcher on tree like so :

    	tree.on('update', (e) => {
    		const eventData = e.data;
    	});
    

    In eventData I get previous, currentData, AND paths, and transactions !

    Is it possible to retrieve such info from a cursor set directly on a nested value ?, such as :

    	const cursor = tree.select('model', 'list1');
    

    In the first case, the transaction gives the actual path of the event, giving the index of the targeted object of an array In the second case, we only get the path of the cursor, not the affected element in the cursor's array

    Thanks ! :)

    opened by Gby56 2
  • Support ES Modules

    Support ES Modules

    It was broken in version 2.5.2.

    opened by anthony-redFox 3
  • Integration with flowtype

    Integration with flowtype

    It would be really nice to be able to have static type checking with Flow while using baobab.

    I didn't push Flow that far though, so I don't know if it's possible at the moment. I could give a try if you are interested :)

    enhancement 
    opened by ThomasCrevoisier 5
  • Monkey in an array

    Monkey in an array

    Hey all,

    Just ran into this bug. If I create a monkey that contains an array within its own path on the tree, it doesn't seem to be able to act as a getter. I'm currently on version 2.4.1. Here's some code to reproduce.

    #!/usr/bin/env node
    'use strict';
    const Baobab = require('baobab');
    const monkey = Baobab.monkey;
    
    
    function alias(item) {
    	return item;
    }
    
    let tree = new Baobab({
    	stuff: 'things',
    	alias: monkey({
    		cursors: {data: ['stuff']},
    		get: alias,
    	}),
    });
    console.dir(tree.get());
    console.dir(tree.get(['alias']));
    console.log();
    
    tree = new Baobab({
    	stuff: 'things',
    	aliases: [
    		monkey({
    			cursors: {data: ['stuff']},
    			get: alias,
    		}),
    	],
    });
    console.dir(tree.get());
    console.dir(tree.get(['aliases', 0]));
    console.log();
    
    tree = new Baobab({
    	stuff: 'things',
    	aliases: [
    		{
    			item: monkey({
    				cursors: {data: ['stuff']},
    				get: alias,
    			}),
    		},
    	],
    });
    console.dir(tree.get(), {depth: 5});
    

    And its output:

    { stuff: 'things', alias: [Getter] }
    { data: 'things' }
    
    { stuff: 'things',
      aliases:
       [ MonkeyDefinition {
           type: 'object',
           getter: [Function: alias],
           projection: [Object],
           paths: [Object],
           options: {},
           hasDynamicPaths: false } ] }
    MonkeyDefinition {
      type: 'object',
      getter: [Function: alias],
      projection: { data: [ 'stuff' ] },
      paths: [ [ 'stuff' ] ],
      options: {},
      hasDynamicPaths: false }
    
    { stuff: 'things',
      aliases:
       [ { item:
            MonkeyDefinition {
              type: 'object',
              getter: [Function: alias],
              projection: { data: [ 'stuff' ] },
              paths: [ [ 'stuff' ] ],
              options: {},
              hasDynamicPaths: false } } ] }
    

    Will probably rearchitect how I'm approaching the problem to work around this as I don't know the performance impact anyways.

    opened by basicdays 3
  • how to remover a listener

    how to remover a listener

    when I use cursor.on('update',fun) how to remover the listener. No cursor.release() because has other listener

    question 
    opened by sutinghui24 2
  • question on immutable in production

    question on immutable in production

    in the readme regarding immutable, it reads

    immutable boolean [true]: should the tree's data be immutable? Note that immutability is performed through Object.freeze and should be disabled in production for performance reasons.

    What does this mean by should be disabled in production? Do we need to do anything specifically for production?

    Thanks

    opened by shenbin04 6
  • Is

    Is "apply" working as intended when operating on lists/arrays?

    Hi,

    I'm confused by the result of calling apply on a cursor pointing at a list. Looking at the documentation and the function's name, to me it would seem that apply works as follows:

    1. If the cursor value is an object, pass that object to the apply-callback and replace the cursor's value with the result.
    2. If the cursor value is a list / an array, iterate over the list, supplying each element to the callback. Collect the callback results in a new list and replace the original list with the new one (similar to Array.prototype.map).

    This, however, does not seem to be the case, as visible in this short example:

    const Baobab = require('baobab');
    
    const tree = new Baobab({
      list: [0, 1, 2, 3, 4],
      obj: {
        firstName: 'John',
        lastName: 'Doe',
      }
    }, {asynchronous: false});
    
    const haveAnIdentityCrisis = person => Object.assign({}, person, {
      lastName: 'Buck'
    });
    
    console.log(tree.get('obj')); // { firstName: 'John', lastName: 'Doe' }
    tree.apply('obj', haveAnIdentityCrisis);
    console.log(tree.get('obj')); // { firstName: 'John', lastName: 'Buck' }
    
    console.log(tree.get('list')); // [ 0, 1, 2, 3, 4 ]
    tree.apply('list', val => val + 1);
    console.log(tree.get('list')); // 0,1,2,3,41
    

    Apparently it makes no distinction between the cursor value being an object or an array. Is this intended? In this case it would make sense to clarify this in the documentation, especially since it uses a variable called "newList" in the example:

    var inc = function(nb) {
      return nb + 1;
    };
    
    // Applying the function
    var newList = cursor.apply(inc);
    
    opened by Nimelrian 2
  • Merge and update event: excessive event handler's calls

    Merge and update event: excessive event handler's calls

    I found the odd behavior of update handler and merge.

    Example:

    const tree = new Baobab();
    tree.set('test', {});
    tree.select('test', 'value').set(1);
    
    // Add update event handler
    tree.select('test', 'value').on('update', (e) => console.log('update:', e.data.currentData, e.data.previousData)); 
    
    
    tree.select('test', 'value').set(2)
    // Update handler's output: update: 2 1
    
    tree.select('test', 'value').set(2)
    // Update handler wasn't called
    
    tree.select('test', 'value').set(3)
    // Update handler's output: update: 3 2
    
    tree.select('test', 'anothervalue').set(1);
    // Update handler wasn't called because it is another cursor
    

    Before, everything was OK. Update handler was called only when data was changed.

    tree.select('test').merge({ 'anothervalue': 1});
    // Update handler's output: update: 3 3
    
    tree.select('test').merge({ 'anothervalue': 1});
    // Update handler's output: update: 3 3
    

    I think it is bad behavior because data wasn't changed and cursor ['test', 'value'] always had the same value.

    What do you think about this?

    question 
    opened by ruscoder 2
  • Fix history .undo() - unnecessary condition

    Fix history .undo() - unnecessary condition

    Hi, I think that throw Error is unnecessary, becouse if in the tree is stored value that returns false in the if condition you can't use methods .undo (), because it always returns throw error. This blocking me, because I have a lot of value in the tree like null, false and i get error when i try undo to this value.

    bug 
    opened by whitcik 4
Releases(2.6.1)
  • 2.6.1(Jan 26, 2021)

  • 2.6.0(Feb 19, 2020)

  • 2.5.3(Dec 2, 2019)

  • 2.5.2(Mar 5, 2018)

  • 2.5.1(Feb 26, 2018)

  • 2.5.0(Sep 29, 2017)

  • 2.4.3(Feb 28, 2017)

  • 2.4.2(Feb 25, 2017)

  • 2.4.1(Feb 6, 2017)

  • 2.4.0(Jan 19, 2017)

  • 2.3.4(Nov 9, 2016)

  • 2.3.3(Jan 22, 2016)

  • 2.3.2(Jan 19, 2016)

  • 2.3.1(Jan 14, 2016)

  • 2.3.0(Jan 5, 2016)

    • Adding the tree/cursor.clone and the tree/cursor.deepClone methods.
    • Adding the tree/cursor.pop and the tree/cursor.shift methods.
    • Adding a way to disable a single monkey's immutability.
    • Fixing an issue where the tree.commit method would fire a useless update.
    • Fixing an issue related to updates and dynamic paths.
    • Fixing the tree/cursor.splice to correctly handle negative indexes.
    • Fixing a bug related to eager monkeys and immutability.
    Source code(tar.gz)
    Source code(zip)
  • 2.2.1(Dec 14, 2015)

  • 2.2.0(Dec 11, 2015)

  • 2.1.2(Nov 26, 2015)

    • Storing hashed paths using λ as delimiter instead of / to enable some edge cases (@nivekmai).
    • Fixing an issue with cursors where a stopped history wouldn't restart correctly (@nikvm).
    • Fixing monkeys' laziness.
    • Fixing an edge case when one watches over paths beneath monkeys.
    Source code(tar.gz)
    Source code(zip)
  • 2.1.1(Nov 10, 2015)

  • 2.1.0(Oct 19, 2015)

  • 2.0.1(Oct 15, 2015)

  • 2.0.0(Sep 28, 2015)

    • The tree is now immutable by default.
    • Cursor's setters method won't return themselves but rather the affected node now.
    • Adding cursor.concat.
    • Adding cursor.deepMerge.
    • Adding cursor.serialize.
    • Adding cursor.project.
    • Adding cursor.exists.
    • Adding tree.watch.
    • Adding the pure option.
    • Changing the way you can define computed data in the tree, aka "facets". Facets are now to be defined within the tree itself, are called "monkeys", and can be accessed using the exact same API as normal data.
    • Adding an alternative dynamic node definition syntax for convenience.
    • Dropped the syncwrite option. The tree is now writing synchronously but still emits its updates asynchronously by default.
    • Max number of records is now set to Infinity by default, meaning there is no limit.
    • Update events are now exposing the detail of each transaction so you can replay them elsewhere.
    • Fixing cursor.push/unshift behavior.
    • Dropped the $cursor helper.
    • Dropped the update specs for a simpler transaction syntax.
    • Updated emmett to 3.1.1.
    • ES6 codebase rewrite.
    • Full code self documentation.
    Source code(tar.gz)
    Source code(zip)
  • 1.1.2(Jul 13, 2015)

  • 1.1.1(Jun 16, 2015)

  • 1.1.0(May 8, 2015)

    • Adding an immutable option to the tree.
    • Adding a syncwrite option to the tree.
    • Adding a get and select event to the tree.
    • Facets getters are now applied within the tree's scope.
    • update events are now exposing the related data for convenience.
    • Fixing a $cursor related bug.
    • Fixing type.Primitive.
    • Fixing facet.release issues.
    Source code(tar.gz)
    Source code(zip)
  • 1.0.3(May 4, 2015)

  • 1.0.2(Apr 26, 2015)

    • Fixing facets related issues (internal).
    • Fixing cases where falsy paths in cursors setters would fail the update.
    • Fixing $splice behavior.
    • Fixing $merge behavior.
    • Persistent history rather than deep cloned.
    • Improving performances on single update cases.
    Source code(tar.gz)
    Source code(zip)
  • 1.0.1(Apr 22, 2015)

    • Fixing scope argument of tree.createFacet.
    • Fixing facet mappings edge cases.
    • Facets can now use facets.
    • Fixing merge edge cases.
    • Fixing update edge cases.
    • Fixing bug where setting falsy values would fail.
    Source code(tar.gz)
    Source code(zip)
  • 1.0.0(Apr 20, 2015)

    • Dropping cursor.edit and cursor.remove in favor of cursor.set and cursor.unset polymorphisms.
    • Dropping typology dependency.
    • Dropping options: clone, cloningFunction, singletonCursors, shiftReferences, maxHistory, mixins and typology.
    • Updated emmett to v3.0.0.
    • Moving react integration to baobab-react.
    • Shifting references is now default.
    • Adding facets.
    • Adding $splice keyword and cursor.splice.
    • Adding validationBehavior option.
    • Adding $cursor paths.
    • Adding path polymorphisms to every cursor's setters.
    • Reworking history to work at cursor level.
    • Reworking validation process.
    • Fixing some bugs.
    Source code(tar.gz)
    Source code(zip)
  • 1.0.0-rc1(Apr 16, 2015)

Owner
Guillaume Plique
Guillaume Plique
JavaScript & TypeScript persistent and optionally immutable data tree with cursors.

Baobab Baobab is a JavaScript & TypeScript persistent and immutable (at least by default) data tree supporting cursors and enabling developers to easi

Guillaume Plique 3.1k Sep 10, 2021
Immutable data structures with history for top-to-bottom properties in component based libraries like React. Based on Immutable.js

Immstruct A wrapper for Immutable.js to easily create cursors that notify when they are updated. Handy for use with immutable pure components for view

null 375 Jul 15, 2021
Wrapper classes around Immutable.js that turn it inheritable

Wrapper classes around Immutable.js that turn it inheritable Extendable Immutable.js About Ever wished that you could have OrderedMaps, Maps or Lists

Phil Pluckthun 58 Aug 11, 2021
A tree data structure that emits events on updates, even if the modification is triggered by one of the leaves, making it easier to think in a reactive way.

Freezer A tree data structure that emits events on updates, even if the modification is emited by one of the leaves, making it easier to think in a re

Javier Marquez 1.3k Aug 27, 2021
Morearty.js - centralized state management for React in pure JavaScript

Morearty.js ![Gitter](https://badges.gitter.im/Join Chat.svg) Introduction Download Dependencies Current status Documentation Usage TodoMVC App compon

null 671 Aug 9, 2021
An immutable data store for managing deeply nested structure with React

Cortex is an immutable data store for managing deeply nested structure with React Key features: supports deeply nested data uses immutable data, which

Quan Nguyen 1.1k Sep 8, 2021
React integration for Baobab.

baobab-react Welcome to baobab's React integration (from v2.0.0 and onwards). Implemented patterns: Hooks Higher order components (curried so also usa

Guillaume Plique 308 Aug 20, 2021
Create the next immutable state by mutating the current one

Immer Create the next immutable state tree by simply modifying the current tree Winner of the "Breakthrough of the year" React open source award and "

immer 21.2k Sep 24, 2021
A light-weight type-safe Elm-like alternative for Redux ecosystem, inspired by hyperapp and Elmish

Hydux A light-weight Elm-like alternative for Redux ecosystem, inspired by Hyperapp and Elmish. Contents Hydux Contents Features Try it online! Instal

null 219 Sep 6, 2021
🔄 A realtime Database for JavaScript Applications

RxDB A realtime Database for JavaScript Applications RxDB (short for Reactive Database) is a NoSQL-database for JavaScript Applications like Websites,

Daniel Meyer 16k Sep 25, 2021
React, but with built-in global state management.

ReactN ReactN is an extension of React that includes global state management. It treats global state as if it were built into React itself -- without

Charles Stover 1.9k Sep 16, 2021
A library for writing React components that automatically manage subscriptions to data sources simply by accessing them

ReSub A library for writing better React components and data stores. Uses automatic subscriptions to reduce code and avoid common data flow pitfalls.

Microsoft 605 Aug 12, 2021
The state manager ☄️

☄️ effector The state manager Table of Contents Introduction Effector follows five basic principles: Installation Documentation Packages Articles Comm

effector ☄️ 3.4k Sep 20, 2021
A tiny and unobtrusive state management library for React and Preact apps

statty A tiny and unobtrusive state management library for React and Preact apps The current size of statty/dist/statty.umd.min.js is: The problem Mos

Alessandro Arnodo 514 Aug 17, 2021
Highly Composable MVVM Framework for React

Astarisx Highly Composable MVVM Framework for React. Click here for the Astarisx Website and Documentation Highly Composable UI Components Astarisx Vi

Zuudo 86 Aug 13, 2019
🍉 Reactive & asynchronous database for powerful React and React Native apps ⚡️

A reactive database framework Build powerful React and React Native apps that scale from hundreds to tens of thousands of records and remain fast ⚡️ W

Nozbe 7.8k Sep 23, 2021
Async rendering & data-fetching for universal React applications.

React Resolver Async-rendering & data-fetching for universal React applications. React Resolver lets you define data requirements per-component and wi

Eric Clemmons 1.7k Sep 11, 2021