Yomguithereal / baobab

JavaScript & TypeScript persistent and optionally immutable data tree with cursors.
MIT License
3.15k stars 115 forks source link

Using monkeys isomorphically #381

Open tomconroy opened 8 years ago

tomconroy commented 8 years ago

Is this possible? I'd like to use monkeys to simplify and optimize reducing data, but it seems impossible in my use case: server grabs some data, sets state in the tree, writes the tree as JSON to the page, browser picks up the 'initial tree' and initializes with new Baobab(window.initialTree). When it does this, the monkeys have been serialized and thus duplicated. Perhaps there's a different serialize method that omits monkeys (serializeRaw ?) and then we could initialize the tree with the empty data + monkeys, then tree.set(window.rawData). What do you think?

Edit: Here's an example of the use case

Yomguithereal commented 8 years ago

Hello @tomconroy. Your use case is a really interesting one. I am trying to understand what you want to do but your example repo does not seem to use monkeys.

Do you mean that some computation done by your server-side monkeys is heavy and that you want to avoid running it one more time on the clients?

Concerning serialize, it should return your tree's data without the computed part. If this is not the case then we have a problem.

tomconroy commented 8 years ago

Ah, right, I missed the part where serialize excludes computed data.

Do you mean that some computation done by your server-side monkeys is heavy and that you want to avoid running it one more time on the clients?

Not really, I just want to initialize the tree with data already sent from the server but keep the monkeys in the tree.

I think there's a still a problem here, I'm still having problems initializing a tree with existing raw/uncomputed data, when I do this it seems to override the monkey keys:

var tree = new Baobab({
  user: {
    name: '',
    surname: '',
    fullname: Baobab.monkey({
      cursors: {
        name: ['user', 'name'],
        surname: ['user', 'surname']
      },
      get: function(data) {
        return data.name + ' ' + data.surname;
      }
    })
  }
})

tree.set(['user', 'name'], "Foo")
tree.set(['user', 'surname'], "Bar")
tree.get('user', 'fullname') // => "Foo Bar"

tree.set({user: {name: "John", surname: "Smith"}})
tree.get('user', 'name') // => "John"
tree.get('user', 'fullname') // => undefined

This seems to make sense that set overrides the whole tree (and erases the monkeys)

I tried using deepMerge with different but incorrect results (the data that should be computed seem to be static instead of computed now):

tree.deepMerge({user: {name: "John", surname: "Smith"}})
tree.get('user', 'fullname') // => "John Smith"
tree.set(['user', 'name'], "Yom")
tree.get('user', 'fullname') // => "John Smith"

So, perhaps deepMerge needs to be cleverer about dealing with raw and computed data for this situation?

This is library has already changed my life and if we can get this working it would be amazing. I'll see if I can help.

Yomguithereal commented 8 years ago

Typically, what I do in this case is to merge a default state (containing monkeys) and actual state beforehand (with lodash typically, so I avoid an unnecessary update of the tree) and then I instantiate the tree with it. However, the deepMerge thing should also work and I remembered having to code some things to make it work. So, if you tell me your use case does not work, then this is not to be expected and I have to fix things and make it work again.

tomconroy commented 8 years ago

Right, that makes sense, and seems to work, with one caveat: if there are any keys in the "defaults" data that get removed by the server after operating on the tree, they'll exist in the client version after doing _.merge(defaults, initialData) and the initial state tree is inconsistent.

e.g. defaults (shared between server & client)

var defaults = {
  foo: {
    bar: "baz"
  }
}

server:

var tree = new Baobab(defaults)
tree.set('foo', {fizz: 'buzz'})
tree.get() // => {foo: {fizz: 'buzz'}}
// insert tree with html response:
<script>window.initialData = JSON.stringify(tree.serialize())</script>

client:

var data = _.merge(defaults, window.initialData)
// => {foo: {bar: 'baz', fizz: 'buzz'}}

This is maybe a trivial example that can be easily avoided but I ran into this problem the first time I tried this method. Without monkeys it's as simple as new Baobab(initialData)

Does this make sense? I'm imagining a Baobab method unserialize (?) that resets the non-computed data... I haven't dug around in the source code though, maybe this isn't a trivial problem.

markuplab commented 8 years ago

We use Baobab in isomorphic application. When we get html code from server, we rerun all state operations (we use cerebral-like state manager - https://github.com/markuplab/appstate). But main problem is http requests. We send serialized responses from server to client, and cache next http requests that will be executed during state modification. It's allow fast state restore on client-side. Also we don't rerender our application on client, we just bind event handlers, because use morphdom instead virtual dom. I think it's help you.

tomconroy commented 8 years ago

we don't rerender our application on client, we just bind event handlers

React does this too if the app state is accurately restored.

markuplab commented 8 years ago

Look at example:

var actions = require('./actions');

// Signal
exports.page = [
  actions.setFirstName,
  actions.setLastName,
  actions.setComputed
];

// actions.js
module.exports = {
  setState (args, state) {
    state.set('first', 'Kirill');
  },

  getState (args, state) {
    state.set('last', 'Kaysarov');
  },

  setComputed (args, state) {
    state.set('fullName', Baobab.monkey({
      cursors: {
        first: ['first'],
        last: ['last']
      },
      get: function(data) {
        return data.first + ' ' + data.last;
      }
    }))
  }
};

You can just run it twice, first time on server and second run on client for restore. It's simple way, and it's also easy reusable and supportable.

Yomguithereal commented 8 years ago

@tomconroy: I don't see computed data being used in your example. Maybe the server part of your example should use a merge rather than a set? Or why, if the server already packs the defaults, can't the client just use window.initialData since default have already been processed?

tomconroy commented 8 years ago

Because the initialData doesn't have any monkeys. I need the server's computed tree to look the same in the client after initializing. I think your suggestion of doing new Baobab(_.merge({}, defaults, initialData)) will work for this use case. We just need to be careful not to remove any keys (that appear in the defaults) in any updates on the server, since when we do and then _.merge, the client will have data that the server won't, and the "isomorphic mounting" process, the transition from server -> client will be inconsistent. I'll update the example soon to demonstrate

Yomguithereal commented 8 years ago

Yes. That's right. One could see the problem likewise: your UI is a pure function and the tree state is actually the arguments you pass to such a function. So, if you tree is not exactly the same, then your UI should not really be rendered the same. The tree's structure is therefore important. But I start to see why in your case it is more complex than average.

markuplab commented 8 years ago

https://github.com/markuplab/catbee-todomvc - Isomorphic TodoMVC based on Baobab

Yomguithereal commented 8 years ago

Thanks @markuplab. Will check this.