Closed kristianmandrup closed 9 years ago
Is there some way to tell the component which states in the global or local state object should cause a redraw of the component on change?
If you use mercury.partial
, the component will be rendered any time it gets a new state object. With observ-*
(or any immutable data structure), this means a component will only be rendered when the state has changed. The state is considered different when oldState === newState
is false
. This will work the same with computed values.
Thanks @nrw
Yes, thunk can be generated using mercury.partial
as you say:
partial: require('vdom-thunk'),
You can use vdom-thunk
to effectively memoize a function that returns a virtual DOM node.
This means if you call it twice with the same arguments it will not re-evaluate the function.
function render(state) {
return h(".todomvc-wrapper", {
"style": { "visibility": "hidden" }
}, [
h("link", {
rel: "stylesheet",
href: "/mercury/examples/todomvc/style.css"
}),
h("section#todoapp.todoapp", [
mercury.partial(header, state.field, state.events),
mainSection(state.todos, state.route, state.events),
mercury.partial(statsSection,
state.todos, state.route, state.events)
]),
mercury.partial(infoFooter)
])
}
function header(field, events) {
return h("header#header.header", {
"ev-event": [
mercury.changeEvent(events.setTodoField),
mercury.submitEvent(events.add)
]
}, [
h("h1", "Todos"),
h("input#new-todo.new-todo", {
placeholder: "What needs to be done?",
autofocus: true,
value: field.text,
name: "newTodo"
})
])
}
function mainSection(todos, route, events) {
var allCompleted = todos.every(function (todo) {
return todo.completed
})
var visibleTodos = todos.filter(function (todo) {
return route === "completed" && todo.completed ||
route === "active" && !todo.completed ||
route === "all"
})
return h("section#main.main", { hidden: !todos.length }, [
h("input#toggle-all.toggle-all", {
type: "checkbox",
name: "toggle",
checked: allCompleted,
"ev-change": mercury.valueEvent(events.toggleAll)
}),
h("label", { htmlFor: "toggle-all" }, "Mark all as complete"),
h("ul#todo-list.todolist", visibleTodos.map(function (todo) {
return todoItem(todo, events)
}))
])
}
I don't see any reason why not to use .partial
... Why is it not used for mainSection
here?
What about the h
elements themselves? I would imagine they are be memoized too?
mainSection
looks really "ugly" in this example. Why not make it into a real component instead of mixing logic, state and rendering in one function? if todos, route and events are the same as before it should use the previous memoized rendition of mainSection? or are there limitations to this?
Not sure where to ask such questions...
Now the only problem with this approach is that the state is local
The state is not local, it just looks like local state! This is exactly what you want, all the simplicity of a single global state atom and all the modularity of local state.
Specifically you can take the component and put it in your app
var myApp = hg.struct({
evComp: EvComponent(...).state,
evComp2: EvComponent(...).state
})
function render(state) {
h('div', [
EvComponent.render(state.evComp),
EvComponent.render(state.evComp2)
]);
}
mercury.app(document.body, myApp, render);
this means that if you want to see the state of all components you can just take a look at myApp
and all the state will be in there somewhere (it might be nested 5 objects deep though! ).
I don't fully understand the rendering process yet. Is there some way to tell the component which states in the global or local state object should cause a redraw of the component on change?
We use a technique called cursors which is similar to what om ( https://github.com/swannodette/om ) does.
Basically in this example:
var myApp = hg.struct({
evComp: EvComponent(...).state,
evComp2: EvComponent(...).state
})
function render(state) {
h('div', [
EvComponent.render(state.evComp),
EvComponent.render(state.evComp2)
]);
}
mercury.app(document.body, myApp, render);
We cause a re-render when anything in myApp
changes. That's how mercury.app()
works, it means if anything in myApp
changes call render()
again and do a diff.
Now the way that this "local state" works is the technique called cursors. When myApp.evComp.isOpen
changes we create both a new myApp.evComp
object and a new myApp
component object. i.e. if you mutate a deeply nested key it propagates upwards and creates new shallow clones of the parents. This means your state is always immutable and this means that when any deeply nested state changes we create a new top level atom.
Because we create a new top level atom it causes mercury.app()
to go "Hey, the top level atom changed because something inside it changed, redraw!"
Read more about the life cycle here ( https://github.com/Raynos/mercury/blob/master/docs/life-cycles.md ).
Feels so much more efficient than walking through the entire graph...??
Redrawing the entire application is actually cheap. Let's make a few assumptions.
We have a caching technique called hg.partial()
which means that any subsections of your app that have not changed will not re-render or re-diff.
Let's assume that two pieces of state changed in a frame. You will have to re-render those rendering functions for the state that has changed, you can never avoid that. Since those dom elements that have changed are at most 20 dom elements deep, you will have to re-render their parents because we go top down, you will have about 5-6 parent components at most.
So we had to re-render 14 components because two changes happened. However all those other components that are not in direct parents path of the changed components are cached and not re-evaluated.
This means that a full redraw without any caching would have had to redraw thousands of components but because of the caching we only redraw what's changed and the parents.
Now this sounds great but it has a caveat. If one of the parents has a list of 10000 child components you still have to loop through that list and create 10000 partial thunks. We do not really have a good solution to speeding up a list with a huge amount of children. I suspect the best solution would be culling but dont have a proven example for it.
I don't see any reason why not to use .partial... Why is it not used for mainSection here?
It should be used on mainSection
! the fact that it is not is a bug.
What about the h elements themselves? I would imagine they are be memoized too?
There is overhead in using thunks, they are not free. For something as small and atomic as a single h()
element there is not much value in it.
However you could indeed memoize h()
since we uniquely have an immutable virtual dom. You would have to write a very clever memoizer though because you could quickly balloon to 500 megabytes of memory trying to memoize every permutation.
mainSection looks really "ugly" in this example. Why not make it into a real component instead of mixing logic, state and rendering in one function?
There is no mixing of logic, state and rendering in one function. There is no state in that function.
What you see in that function is called non-trivial rendering logic. There is always a design question in terms of writing frontend apps about "how much stuff should i put in a viewmodel as computed properties" vs "how much rendering logic should I put in my template".
It sounds like i put too much rendering logic in the template for your liking (I actually like this approach). Your welcome to use more computed properties and have dumber templates, i tried that approach and found it uglier.
if todos, route and events are the same as before it should use the previous memoized rendition of mainSection? or are there limitations to this?
Correct, if those three values are referentially equal (i.e. todos1 === todos2, route1 === route2, events1 === events2 ) then it will not re-evaluate that function and just use the previously cached vnode.
i just want to say, i'd been reading and pondering virtual-dom frameworks for days now, and have been trying to wrap my head around mercury. this thread is what finally made it click, i hope that you will or have preserved this in FAQs or something, as it may help others!!
Please summarise the key points on a new wiki page ;)
Not a wiki page :( the wiki got corrupted / trolled. you can contribute docs in PRs on docs folder.
Been frustrated at the lacking documentation. Apparently the docs are mostly to be found in the
/docs
folder of the source code!! And the best docs I have found so far on how to structure a real app is theFAQ.md
.I found this little gem in there, showing how to define a Component. Now the only problem with this approach is that the state is local, but of course we could just as well reference global state and use some sort of Flux mechanism to mediate changes between components and global app state similar to React...
I don't fully understand the rendering process yet. Is there some way to tell the component which states in the global or local state object should cause a redraw of the component on change? Feels so much more efficient than walking through the entire graph...?? How about Array Proxies or proxies in general? If we have an iteration within the render, it should always render a new component on each iteration and that component should only re-render if its state changed relative to last time it was rendered. Is this possible currently?
How about computed properties?
But haven't seen any real app examples using this feature...