Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

View state composition? #8

Closed
leeoniya opened this issue Jan 21, 2016 · 22 comments
Closed

View state composition? #8

leeoniya opened this issue Jan 21, 2016 · 22 comments
Labels

Comments

@leeoniya
Copy link
Member

Hey, @dustingetz

Continuing the discussion from HN: https://news.ycombinator.com/item?id=10942343

I dont think this is the type of composition I am talking about. This composes pure views, like react, but it doesn't compose their state, same as react.

View state is, in fact, retained in both declarative and imperative style composition, unless I'm not understanding.

Would you mind clarifying and/or providing a pattern you're looking for?

thanks!

@leeoniya leeoniya changed the title View state composition View state composition? Jan 21, 2016
@leeoniya
Copy link
Member Author

@dustingetz

If you'd like, I can re-implement your react-cursor demo app [1] using domvm :)

[1] http://react-json-editor.bitballoon.com/examples/react-state-editor/webapp/

@dustingetz
Copy link

The beauty of the state-at-root pattern with cursors, is that each little component, each subtree of the view, can stand alone as its own little stateful app, and they naturally nest/compose recursively into larger apps. This fiddle is meant to demonstrate this: https://jsfiddle.net/dustingetz/n9kfc17x/ - none of the downtree views own state, they are stateless, their state is externalized to the root store but their state still nests properly.

@dustingetz
Copy link

@leeoniya
Copy link
Member Author

that jsfiddle doesnt render anything for me :(

I don't come from React, so without a working fiddle (or JSX highlighting), it's a bit hard to grok.

@dustingetz
Copy link

Yes i would very much like to see the json editor built on other platforms. (The source code to that thing is two years old, it wont be that much help to you, i havent gotten around to upgrading it and all its dependencies yet as that's a rewrite)

@leeoniya
Copy link
Member Author

@dustingetz I don't know why the fiddle didn't work before, but it worked on the 10th try.

Here's the entire thing implemented in domvm. You'll notice the first 14 lines are the actual cursor :)

https://jsfiddle.net/fxzr0fht/

// creates a portable getter/setter that emits
// redraw requests, can be used recursively
function createCursor(parent, childKey, vm) {
    var oldVal = parent[childKey];
    return (newVal) => {
        if (typeof newVal === "undefined")
            return oldVal;
        else {
            oldVal = parent[childKey] = newVal;
            vm.emit.redraw();
        }
    };
}

function App(vm, state) {
    var countersCursor = createCursor(state, "counters", vm);
    return () =>
        ["div",
            ["h1", "App cursor:"],
            ["p",
                ["code", JSON.stringify(state, undefined, 2)],
                [CounterList, countersCursor],
            ]
        ]
}

function CounterList(vm, countersCursor) {
    var clickers = countersCursor().map((val, idx) => [Clicker, createCursor(countersCursor(), idx, vm)]);
    return () =>
        ["div",
            ["h2", "CounterList cursor:"],
            ["p", ["code", JSON.stringify(countersCursor(), undefined, 2)]],
            clickers,
        ]
}

function Clicker(vm, itemCursor) {
    return () =>
        ["div",
            ["input", {type: "text", value: itemCursor(), readonly: true}],
            ["button", {onclick: () => itemCursor(itemCursor()+1)}, "+1"],
            " ",
            ["span",
                "Clicker cursor:",
                ["code", itemCursor()],
            ]
        ]
}

// state/model/whatever
var state = {
    counters: [0,0,0,0,0,0,0,0,0,0]
};

domvm(App, state).mount(document.getElementById("root"));

@dustingetz
Copy link

Right, but does your library naturally do this without the cursor? If you're just going to implement cursors anyway, what advantage does this have over react + react-cursor?

@dustingetz
Copy link

My point is that it's the cursor abstraction that is providing the additional composition power here, not the underlying vdom implementation.

@leeoniya
Copy link
Member Author

The abstraction point is a bit disingenuous.

First, the cursor implemented above is not specifically for this app, it is generic and will work with any domvm app - not bad for 10 LOC.

Second, react-cursor is itself an abstraction over sub-state and React in turn is a mutation/observer abstraction. Since js doesnt have native mutation observers, you're stuck anyways with writing abstractions that trigger vdom redraw - either React's setState(), or es5 getters/setters or a cursor/wrapper. EDIT: Mithril has an auto-magic global redraw, which I was able to implement completely externally to domvm in ~100 LOC and with extra features [1], it's probably broken since some lib semantics have changed recently. It needs some love soon.

The benefit over React? Well, there's code size, and the fact that domvm is massively faster than React. The template syntax is pure js, concise and pleasant which is familiar to every JS dev (all of React's audience) and requires 0 tools, dependencies or compilation. The total code size loaded onto the client is the code above plus an ~8k domvm lib, that is it.

There is actually a lot more to like, but I want to restrict the context to this example. I'm not trying to convert you as you're already heavily invested in React and its ecosystem (and React Native) seems unstoppable. However, I don't agree with React's philosophy. Like you I want my components to live outside of the DOM tree and expose their own APIs with no overhead. It's what makes components based on a domvm view layer truly reusable and not framework dependent....though the specific example above was intentionally structured to mimic the provided fiddle, so it's very UI-centric, like React.

[1] https://github.com/leeoniya/domvm/blob/master/src/watch.js

@leeoniya
Copy link
Member Author

btw, pretty much all of React's perf problems will be solved by switching to the drop-in replacement https://github.com/trueadm/inferno, check it out.

Again, my philosophy is just very different than React's (which is designer-centric and monolithic - hence JSX) For me, simply a speed increase is far from what I'm looking for in a component architecture. My js journey has been jQuery => Mithril => domchanger => writing domvm, though I've tried plenty of others along the way.

I have a simple litmus test for the term "component", because everyone throws it around as if it's synonymous not only with encapsulation but re-usability. CodeMirror is an example of a truly reusable component: it is encapsulated, has a powerful API and can be embedded in ANY web app. A simple question is, can CodeMirror be rewritten in a reusable fashion using "framework X" to be used anywhere except within the strict confines of "framework X". The answer in the case of React is a resounding "no". In the case of domvm, the answer is "absolutely". You can write components (just plain js domain models) and use domvm as their view layer to gain all the benefits of declarative templates, vdom speed, flexible view composition, separation of concerns (if needed), normal API exposure and isomorphism.

The fact that the example above was implemented almost line-for-line without needing to understand any foreign concepts and relying only on 8k worth of view layer code to get the job done much faster than React speaks for itself, IMO.

@dustingetz
Copy link

For my original question, I care only about how composable the application model is (markup, store/state, actions/events) and what parts of the component model compose recursively and what parts don’t, i don't care about optimizations. This is different than the types of things you seem to be interested in. We aren’t really communicating too well in this thread, we’re using different ideas of what a component is and different ideas on what composition means. If the topic is interesting we can keep talking though, it is interesting research to me, what do you think?

(for the record, react-cursor is fully general and works outside of react, it will work with domvm)

@leeoniya
Copy link
Member Author

so i'm thinking about how to do the json editor. it's fairly simple to just make something that edits only the values using some recursion and cursorizing. however, this type of editor is fairly limited in usefulness. what would be more interesting is to make an editor that can edit the structure as well - reorder, add/remove and re-nest nodes as well as edit values.

any thoughts?

@leeoniya
Copy link
Member Author

the more i think about it, the more it make sense to create a parallel vJson structure that's exclusively array based since objects are not cheaply reordered (nor is order guaranteed) and keys not easily altered. a solution that edits a json structure in-place would be quite complex for no real benefit over an easily modifed uniform structure with a .toJson() method...

@leeoniya
Copy link
Member Author

Here's an initial stab at it: https://jsfiddle.net/5ymq4yc6/1/

DONE

  1. Live key and value editing
  2. Value validation
  3. Event delegation, only a single keyup handler is bound at the root level.

TOFIX

  1. keys use same type validation rules as values, but keys cant be null/true/false or mismatched to parent container type
  2. value validation doesnt handle escapes or 1e4 notation

TODO

  1. expose method to convert internal struct back to json/object
  2. node adding/removing
  3. node reordering (drag/dop)
  4. node re-nesting (drag/drop)
  5. maybe use even more granular vm.patch() instead of vm.redraw() for changes with no side-effects

@dustingetz
Copy link

I see now that domvm idiomatic style mutates state objects directly - how do you implement lifecycle methods and the equivalent of shouldComponentUpdate?

@leeoniya
Copy link
Member Author

I see now that domvm idiomatic style mutates state objects directly

yes, immutability has some benefits especially on huge teams. but if a team chooses to adhere to immutability, then it can be enforced by idiomatic codigin style rather than something that must always be worked around else it breaks the framework. if you don't want to mutate the state, then don't. the key to workable mutability is just being able to "hook" state mutations to ensure sync. i decided to leave that decision up to the implementor rather than baking it in when the language supports it anyhow.

how do you implement lifecycle methods

Lifecycle hooks are currently limited to vm-level willRedraw/didRedraw, willDestroy/didDestroy [1]. I'm still hashing out some uniformity for providing granular per-element hooks and not all of them make sense since dom nodes can get reused and there's ambiguity with update/destroy/recreate, etc.

They're accessed the same way internally and externally via:

vm.on({
    didRedraw: function() {...}
});

// or
vm.on("didRedraw", function() {...});

the equivalent of shouldComponentUpdate

While React discourages forceUpdate, domvm's redraw() does just this. But it can be bubbled up through the composed view tree as a redraw() request, rather than a notification of self redraw.

// redraw self and children
vm.redraw()

// bubble redraw request to root
vm.emit.redraw()

// ...which is a shortcut to this level-targeted event emission
vm.emit("_redraw:1000")

// so you can just trigger a parent redraw via
vm.emit("_redraw:1")

// TODO: targeted at a specific ancestor by key
vm.emit("_redraw:someKey")

// other custom events can be targeted or not
// if not, then they get invoked on every ancestor that has a matching listener
// (it's better to do a targeted _redraw because every vm implicitly has a listener for this)
vm.emit("myEvent", arg1, arg2...)

This way you can be as granular as you need for optimization. If you want something similar to .setState(), you can easily create an redraw-requesting/invoking setter, which is what the cursor does in the counters example.

domvm's redraw-emitting mutation observers (watch.js) are implemented similar to this and mimick Mithril's m.prop(), m.withAttr(), etc.., but much more powerful as they can be used externally as global subscribers and can serve to auto-redraw disjoint apps together. But that's another discussion :)

@dustingetz
Copy link

I am confused, how can you claim to be high performance without implementing render subtree pruning?

@leeoniya
Copy link
Member Author

it's not intuitive at first how such a thing is possible. but what most dont realize is that the bottleneck is in fact the DOM itself rather than a vtree rebuild/diff. The author of Inferno and I had a good discussion about this in another thread. I can link you if you'd like. The key is to do the optimal dom mutations. The overhead of a full diff is quite minimal and becomes only even measurable at truly enormous node counts when the diff is 0.

I don't claim anything that is not backed by real benchmarks against other libs. See the readme for links to examples. I can go into more details if you're really interested, but I'll just link you to the thread first.

Anyways, vm.redraw() will only diff/refresh that view and children.

@dustingetz
Copy link

Please link it. I have worked on quite large react apps using the state-at-root pattern where typing into a form (barely any dom touching, but huge vdiff) destroyed performance in IE8, like 1second renders

@leeoniya
Copy link
Member Author

domvm targets ie9+, though i have just avoided using ie10+ features and have not yet verified that it works perfectly on ie9.

here' the relevant comment:
infernojs/inferno@134a900#commitcomment-15365143

global redraw is not a requirement, as i've stated. you can have granular, external redraw control over each view if you need to optimize. though of course there is nothing stopping me from adding a vm.isDirty flag that can be respected and enabled via config. it would be a trivial addition if someone in fact runs into perf issues. i dont want to add these concepts in prematurely.

@leeoniya
Copy link
Member Author

you're not wrong, of course. with a slow js engine, it can easily become a relevent overhead. if anyone runs into issues such as the one you're describing i will be happy add an isDirty() hook/thunk.

thinking about it i can just allow the willRedraw() hook to return false to prevent redraw, but i want to flesh out the exact semantics before commiting to anything.

@leeoniya
Copy link
Member Author

also see

infernojs/inferno#29 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants