max-mapper / yo-yo

A tiny library for building modular UI components using DOM diffing and ES6 tagged template literals

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

"pure" components

gmaclennan opened this issue · comments

I'm not sure how much of this is within morphdom, hyperx or bel.

How do these modules handle multiple updates to a large dom heirarchy when very little changes? Does morphdom do any simple === comparisons of nodes? If so can we make rendering more efficient by memoizing returned values? e.g. experimenting with yo-yo so far I am writing quite a lot of:

function MyComponent(props) {
  return yo`<div>${props.name}</div>`
}

And all this is composed into a complete page. If yo/bel/hyperx could memoize the returned values for each input of strings and values, then morphdom could do a simple === and save time maybe?

Maybe this is happening already? One potential case is animations - I want to yo.update() the entire page 60 times a second with new values being passed down through the render heirarchy. Most pieces/components of the page don't actually use values, but my understanding is that currently each time a new DOM element would be built, and morphdom would do a deep compare?

Would this be a significant saving? I'm thinking how to reduce both the cost of unnecessary calls of bel.createElement and unnecessary diff calculations.

That is an interesting idea. I wonder if we could memoize the tagged template literal? So basically if the strings and expressions are the same, return the last generated element. Then we could do a isSameNode and avoid calling morphdom at all in yo.update() (unless morphdom already does this).

The main problem with memoizing at the yo-yo/bel level:

var one = yo`<div>hi</div>`
var two = yo`<div>hi</div>`

Both would become the same node and would cause a lot of issues as two.textContent = 'bye' would also change one. AFAIK, .clone(true) is expensive too.

Or maybe there is a different way we could notify yo.update() it's the same element, like setting a data attribute to skip the element if they match?

I have no idea but sounds fun to play around with ideas.

Oh yes, that is a problem. If we memorize at the yo-yo level maybe we could keep a counter that increments every yo.update() and memoize based on that counter. E.g. one and two would render within the same update cycle and memoize based on that

commented

Yeah, one of the downsides of the way the system is designed is that we can't have an equivalent to React's .shouldComponentUpdate() hook so must rely on more exotic solutions.

Or maybe there is a different way we could notify yo.update() it's the same element, like setting a data attribute to skip the element if they match?

I was wondering about that - maybe some efficient tree-hashing function that can traverse values, objects and arrays alike - pops out a single value and sets it in data-hash="hash"? If we can come up with a solution that doesn't require setting multiple properties at the top of each node that'd be neat so the DOM inspector remains usable.

We could use merkle-trees: https://github.com/datproject/docs/blob/master/hyperdrive.md#merkle-trees (nb. edge of my understanding here, but this seems like it might be what is needed)

commented

I was thinking along the same lines as @yoshuawuyts.
Something along the lines of string comparison on of the yo "template" in conjunction with data diffing would work...

I dont know exactly how to approach it, but i always was hoping that there is some way to instead of passing "props", i would pass an "observable" and whenever there is new data, the observable has a reference to the exact locations in the DOM that need patching.

Additionally, what I like about relay/graphql/falcor stuff is that it's possible to specify "data dependencies" inside the components. In case of netflix or facebook, the available data is what they offer, which makes it hard for a component to be used in a completely different context. But, what if an API specification that has to be fulfilled by a backend can be derived from a frontend made up of a bunch of components that specify their data needs automatically?

So having something like:

function MyComponent(props) {
  return yo`<div>${props.name}</div>`
}

would become something like

function MyComponent(db) {
  return yo`<div>${db.watch('name')}</div>`
}

This db.watch('name') would create a write-stream that has a reference to the <div> created by yo in MyComponent and can just div.innerHTML = newName whenever a new value is written to the write-stream...


It's of course a bit more complex.
The "observable" created from a "db" object (which could be a sublevel thing) could watch, but maybe could also write back to the "db" object :-) ...again in a streaming manner.

This would be useful as response to some user interaction.
I wrote https://www.npmjs.com/package/eventhandler-stream in the past, but the "db" part is still missing.

I tried myself on something that does what i try to describe above, but it's still too hard for me to do.
I started here, but not sure if the code is helpful in case the description above is lacking.

I like the idea, but i'm too clumsy to implement it elegantly.

I'd be happy to get some feedback on the idea and whether you feel this approach makes sense at all or not - and what are your thoughts about something "observableish" in general.

commented

I like the idea of being able to specify data constraints within the views / elements comprising the view. Given that any view will always have a notion of "what" data they need, it would be cool to make it explicit without adding the "where" the data comes from. With the onload hook the right data could then be loaded whenever the view is rendered.

This should probably live as a function that wraps bel, but not be integrated into yo-yo / bel / choo itself.


It's of course a bit more complex. The "observable" created from a "db" object (which could be a sublevel thing) could watch, but maybe could also write back to the "db" object :-) ...again in a streaming manner.

This sounds like you're gearing towards a two-way binding solution, which is something that in practice has turned out to generate more complexity than one-way flows do. What use case you you're trying to solve with observables that cannot be done by a unidirectional approach?

I feel they could just as well live at the root of the DOM tree, and flush down objects into a giant object that can in turn be accessed by the views (e.g. props.name syntax). This could all be made nice and abstracted away by a system as proposed in the first secion.

Sorry if I'm rambling a little, I think there's some real cool ideas in here! Does it make sense what I'm saying?

DOM tree thing:
i usually have a "global state thing" as a leveldb that syncs with a backend if necessary.

lots of people seem to use something reduxish having a giant json and reducers following immutability stuff, no? The "immutability" thing to me seems like a performance optimization, so that something can traverse an object fast by checking for changed references.
If the principle is followed strictly, an unchanged reference means, there is no change in that objects subtree.That is my rough understanding of redux. And to me that feels, like updates would happen in O(log(n)) ...but maybe i'm missing something here.

two way binding:
It's not meant as two-way binding :-) The little fictional example code is misleading.
Instead of db.watch('some/foo/bar/thing') (which looks like a duplex stream i guess), there would also be db.readable('some/foo/bar/thing') and db.writable('some/foo/bar/thing'), where the former receives all the updates and the latter only writes updates.

core idea of the previous posts above:
I was thinking, maybe it would be possible that when creating the DOM by rendering a "component tree" to create a lot of "stream instances" which "read/write/both" "from/to/both" the "global local database" (may that be a leveldb or something else - which can be backuped and/or synced with a backend)

for example

// main.js
// ...
var subDB = db.sub('./data/user2')
var mc = MyComponent(subDB)

// mycomponent.js
var imagesnap = require('imagesnap')

module.exports = MyComponent

function MyComponent(db) {
  var selfie$ = db.writable('./selfie')
  var name$ = db.readable('./name')
  return yo`
    <div onclick=${e => imagesnap().pipe(selfie$)}>
      ${name$}
    </div>
  `
}
commented

I'm really starting to love the patterns that are starting to fall out of this POC https://github.com/kristoferjoseph/dam-yo/blob/master/screens/todos-create.js

The division now is looking like views that register for updates and components that are passed state and dispatch.

I'm currently researching the smallest way to add data diffs ( think light weight immutable ) to add a shouldUpdate method.