Yomguithereal / baobab

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

How can I know what is the real path that changed from a monkey update? #438

Open dumconstantin opened 8 years ago

dumconstantin commented 8 years ago

Consider the following:

let tree = new Baobab({
   foo: {
      bam: 123
   },
   bar: monkey(['foo'], identity)
})
tree.select('bar').on('update', e => {
   e.target.actualPathThatChanged // ['foo', 'bam']
   e.target.path // 'bar'
})

tree.select('foo', 'bam').set(321)

What should I replace the e.target.actualPathThatChanged with to get ['foo', 'bam']?

Yomguithereal commented 8 years ago

Hello @dumconstantin. If you need to access paths that were edited you should listen to the tree's update events. They hold this information.

dumconstantin commented 8 years ago

Hey @Yomguithereal, yeah, I tried doing that but then I don't know how to link the monkey update to the tree update because neither knows about the other.

I have a listener on a monkey (that may have other monkeys in its dependency) and a list of paths I don't want the updateFn to be triggered if the changed path is in the list.

My actual use case is like:

let tree = new Baobab({
   a: {
      prop1: { ... },
      prop2: 123,
      prop3: { ... },
      prop4: 'asd'
   },
   b: monkey(['a'], identity)
})

tree.select('b').on('update', e => {
   let exclude = ['prop1', 'prop3']
   if (-1 === exclude.indexOf(e.target.actualPropChange)) {
     doAction()
   }
})

// This is the type of action I don't want to do anything
tree.select('a', 'prop1', 'deepProp').set(321)

// But I want to react to this
tree.select('a', 'prop2').set(321)
Yomguithereal commented 8 years ago

Would having the transaction details of the update in the cursor update event payload help you?

dumconstantin commented 8 years ago

Yes, quite a lot actually.

Knowing exactly what change triggered the monkey to update makes reasoning about the data structure much easier.

Is there a way to achieve that?

Yomguithereal commented 8 years ago

I might add the information to the cursor's payload data then. Just need some time to think about it to see whether it's really relevant or not.

dumconstantin commented 8 years ago

That would be great, I really need it for an app I'm currently building. Do you need for me to describe better my current setup and rationale behind this request?

Yomguithereal commented 8 years ago

Yes. This would be nice.

dumconstantin commented 8 years ago

Sure.

This is what I have at the moment:

// using RamdaJS for the functions
let monkey = Baobab.monkey
let tree = new Baobab({
  entities1: {},
  entities2: {},
  entity2ByEntity1: monkey(['entities2'], compose(map(indexBy(prop('id'))), groupBy(prop('entity1_id')), values)
  visible: {
    entity1: monkey(['entities1'], filter(propEq('visible', true)),
    entity2: monkey(['visible', 'entity1'], ['entity2ByEntity1'], compose(mergeAll, values, useWith(pick, [keys, values]))
  }
})

// I'm using RiotJS and I made a cursor mixin that allows 
// Riot components to subscribe to the baobab state.
// dataCursor:: string (namespace) -> { propName: baobab path }
// The mixin receives an update event, sets the new state on the Riot component
// and then triggers the component to update
<entity2>{ props ... }</entity2>
<entity1>
  <entity2 each={ data.entities2 }></entity2>
  this.dataCursor('data', {
    entities2: ['visible', 'entity2']
  })
</entity1>

<entities>
  <entity1 each={ data.entities1 }></entity1>
  this.dataCursor('data', {
    entities1: ['visible', 'entity1']
  })
</entity>

As I developed the application further the state tree grew as more entities were added. However the data is hierarchical. So the above structure is a bit redundant and monkeys are mostly used to provide the following hierarchy:

entities1 {
   id: {
     props...
     entities2: {
       id: {
          props...
          entities3: { ... }
      }
      entities4: { ... }

If I try to implement the above structure then the ['visible', 'entities1'] monkey will trigger an update every time something happens to any of its children entities. This leads to a huge number of events on the Riot components which ends up refreshing the component without it needing to so.

What I would like to do is this:

<entities>
  <entity1 each={ data.entities1 }></entity1>
  this.dataCursor('data', {
    entities1: {
       path: ['visible', 'entity1'],
       updateOff: ['entities2', 'entities4'] // don't update the state if any of these change
  })

  // or

  this.dataCursor('data', {
    entities1: {
       path: ['visible', 'entity1'],
       updateOn: ['prop1', 'prop2'] // update the state only if these change
    }
  })
</entity>

// and inside the dataCursor something like
tree.select(entities1.path).on('update', e => {
  if (-1 !== e.data.updatedPaths.indexOf(entities.updateOn) {
     tag.data = tree.select(entities1.path).get()
     tag.update()
  } 
})

I don't want to do a comparison between the data in the riot tag and the new data because the state tree should be the single source of truth. Also I wouldn't want to do diffs on the tree on the update function because I expect that cursor events mean the same thing (I don't need to know the value that changed but I do need the path to the value that changed so I can trigger an update).

With the diff/updatePaths are available at the cursor event level I am able to better structure the data tree without any worries of overflowing the app with unnecessary updates on higher level components.

dumconstantin commented 8 years ago

Hmm, I had a thought, maybe adding the monkeys to the tree update event would be a much better (and maybe simpler solution). The way I thought about it now is kind like:

tree.on('update', e => {
  let paths = join(e.data.paths, e.data.monkeyPaths)  
  if (-1 !=== paths.indexOf(RiotTagDataListeningPaths) {
      // trigger an update on all the riot tags according to their updateOn/updateOff rules
  }
})
Yomguithereal commented 8 years ago

Diff should indeed be avoided. The whole point of this library is to avoid costly diffs and rely on paths and referential comparisons enabled by the immutability/persistence system. Which leads me to wonder if this wouldn't be possible to implement someting in RiotJS like React's shouldComponentUpdate which seems to be your issue right now.

dumconstantin commented 8 years ago

Yup, that's exactly what I felt about Baobab and why a big light bulb got lit when I saw it. I'm trying to move every piece of logic onto the Baobab tree, so instead of doing computation inside components (which includes the shouldComponentUpdate) I'm moving that to monkeys - my components are 100% "dumb" in this respect :). So far its been a fantastic way to build the app, much cleaner code and the only place I need to worry about is properly setting up the data structure and the monkeys that will serve what each component needs.

I also need to push data changes to streams or third party APIs, so I can't rely on the internal mechanisms of Riot/React etc to figure out if indeed something needs refreshing. I'm looking at Baobab to provide a universal mechanism to propagate the new state according to what every listener needs.

I was thinking now of using the tree.watch to generate a watcher by parsing what properties each component uses (or doesn't use). I'll let you know how that goes...

jrust commented 8 years ago

I also could see value in having the exact list of paths passed both to cursor changes and monkey callbacks. In our case it's wanting to know only if an item has been added/removed from an array as opposed to just a property on an item changing. Access to the paths makes knowing that easy and allow us to avoid re-computing the monkey data unless an item was added/removed.

dumconstantin commented 8 years ago

I haven't used baobab with arrays yet but I think this is where you'd need the event details most.

As a workaround for nested object, I found using watchers with specific paths to the properties I need, instead of just on the parent object, avoids the problem of recompilation.

Constantin Dumitrescu

On 18 mar. 2016, at 01:59, Jason Rust notifications@github.com wrote:

I also could see value in having the exact list of paths passed both to cursor changes and monkey callbacks. In our case it's wanting to know only if an item has been added/removed from an array as opposed to just a property on an item changing. Access to the paths makes knowing that easy and allow us to avoid re-computing the monkey data unless an item was added/removed.

— You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub