Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Can you expand? You actually convinced other folks to stop using React and go back to writing DOM-manipulating VanillaJS?


No one's going back to the messy spaghetti-generating "just-jQuery" pattern of yore.

I devised a way of using plain closures to create DOM nodes and express how the nodes and their descendants should change when the model changes. So a component is a function that creates a node and returns a function not unlike a ReactComponent#render() method:

props => newNodeState

When called, that function returns an object describing the new state of the node. Roughly:

{ node, className: 'foo', childNodes: [ childComponent(props) ] }

So, it's organized exactly like a react app. A simple reconciliation function (~100 lines) applies a state description to the topmost node and then iterates recursively through childNodes. A DOM node is touched if and only if its previous state object differs from its current state object - no fancy tree diffing. And we don't have to fake events or any of that; everything is just the native platform.

I implemented an in-browser code editor this way. Syntax highlighting, bracket matching, soft wrap, sophisticated selection, copy/cut/paste, basic linting, code hints, all of it... It edits huge files with no hint of delay as you type, select, &c. It was a thorough test of the approach.

Also, when we animate something, we can hook right in to the way reconciliation works and pass control of the update to the component itself, to execute its own transition states and then pass control back to the reconcile function... This has made some really beautiful things possible. Fine grained control over everything - timing, order, &c. - but only when you want it.

Sorry for the wall of text.


I am sorry, but I don't fully understand. To me it sounds like you are describing exactly tree diffing when you say that the next node is only touched if its state object changed.

I have been through this struggle too. Of wanting to get rid of bloated tools and tools I don't understand, and the best I've found for this is Hyperapp. I've read the source code a few times (was thinking about patching it to work well with web components), so I feel it falls into a category of tools I can use. But I'm genuinely interested in understanding what you've done if it offers an alternative (even if more clunky).


>>> it sounds like you are describing exactly tree diffing

The object returned by the function expresses a tiny subset of the properties of a DOM node. Often just {className, childNodes: [...]}. Only those explicit attributes are checked for update or otherwise dealt with by my code. My code has no idea that a DOM node has a thousand properties. By contrast, a ReactComponent is more complex from a JS POV than a native DOM node.

In other words, if my code returns: {className: 'foo'} at time t0, and then returns {} at time t1, the className in the DOM will be 'foo' at t0 and at t1. That is not at all how exhaustive tree diffs work, and not at all how react works.

With 5,000 nodes, you might have 8K-15K property comparisons. Per-render CPU load thus grows linearly and slowly with each new node. I can re-render a thousand nodes in 2-5 milliseconds with no framework churn or build steps or any of that. But more importantly, we have the ability to step into "straight-to-canvas" mode (or whatever else) without rupturing any abstractions and without awkward hacks.

This is unidirectional data flow plus component-based design/organization while letting the DOM handle the DOM: no fake elements, no fake events -- nothing but utterly fast strict primitive value comparisons on shallow object properties.

EDIT: Earlier I said that a node changes if and only if its state description changed; that is not strictly true. "if and only if" should just be "only if".


This makes a lot of sense. It's essentially giving up some "niceness" that React gives to make it faster and closer to the metal. That sounds like a critique, but that's what this whole thread is about, and one way to approach something I've also given a lot of thought.

To do this, I imagine you will have to do certain things manually. I guess you can't just have functions that return a vdom, because, as you say, the absence of a property doesn't mean the library will delete it for you. So do you keep the previous vdom? Patch it manually and then send it off to update the elements? ... I guess it's a minor detail. Doesn't matter.

Interesting approach, thanks for sharing! I will definitely spend some time looking into it. Encouraging that it seems to be working out for you :)


To answer your technical question: You can approach it one of two ways (I've done both). The first you hinted at. You can keep the last state object handy for the next incoming update and compare (e.g.) stateA.className against stateB.className, which is extremely fast. But you have an extra object in memory for every single node, which is a consideration. You can also just use the node itself and compare state.className to node.className. Turns out this is ~90-100% as fast ~95% of the time, and sips memory.

If you're thinking, "wait, compare it to the DOM node? That will be slow!" - notice that we're not querying the DOM. We're checking a property on an object to which we have a reference in hand. I can run comparisons against node.className (and other properties) millions of times per second. Recall that my component update functions return an object of roughly the form:

{ node, className: 'foo', childNodes: [...], ... }

That first property is the DOM node reference, so there's no difficulty in running the updates this way. Things are slower when dealing with props that need to be handled via getAttribute() and setAttribute(), but those cases are <10%, and can be optimized away by caching the comparison value to avoid getAttribute(). There are complications with numerical values which get rounded by the DOM and fool your code into doing DOM updates that don't need to happen, but it's all handle-able.

Here's quick gist: https://gist.github.com/jeffmcmahan/8d10c579df82d32b13e2f449...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: