Raynos / mercury

A truly modular frontend framework
http://raynos.github.io/mercury/
MIT License
2.82k stars 142 forks source link

The FAQ might contain the answers to your questions... #101

Closed kristianmandrup closed 9 years ago

kristianmandrup commented 10 years ago

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 the FAQ.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...

function EventComponent() {
  var events = mercury.input(['toggle']);

  var state = mercury.struct({
    name: 'event name',
    isOpen: mercury.value(false),
    description: 'event description',
    events: events
  });

  events.toggle(function (data) {
    state.isOpen.set(data.value)
  })

  return { state: state }
}

EventComponent.render = function (state) {
  return h('div', [
    h('div', {
      'ev-click': mercury.event(events.eventToggle, {
        eventIndex: j,
        dayIndex: i,
        value: !event.isOpen
      })
    }, event.name),
    h('div', {
      hidden: !event.isOpen
    }, event.description)
  ])
}

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?

var Observable = require("observ")
var computed = require("observ/computed")

var one = Observable(1)
var two = Observable(2)

var together = computed([one, two], function (a, b) {
  return a + b
})

assert.equal(together(), 3)
two.set(5)
assert.equal(together(), 7)

But haven't seen any real app examples using this feature...

nrw commented 10 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.

kristianmandrup commented 10 years ago

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...

Raynos commented 10 years ago

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! ).

Raynos commented 10 years ago

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 ).

Raynos commented 10 years ago

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.

Raynos commented 10 years ago

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.

Raynos commented 10 years ago

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.

cellvia commented 8 years ago

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!!

kristianmandrup commented 8 years ago

Please summarise the key points on a new wiki page ;)

Raynos commented 8 years ago

Not a wiki page :( the wiki got corrupted / trolled. you can contribute docs in PRs on docs folder.