wavesoft/dot-dom

Keyed updates?

mindplay-dk opened this issue ยท 20 comments

Any chance for keyed updates?

Without it, the library isn't much use for forms.

On a related note, does the library do any unnecessary DOM reordering? If so, inputs would lose state as a side-effect of elements before it being added/removed - most VDOM libraries this small have this flaw.

I really like your API for functional components btw... this is much cleaner than hooks, which rely on counting calls and requires a linter. This library is small enough to bundle with custom elements, for example, I could see myself doing that. Nice work! :-)

Hello @mindplay-dk and thanks for your feedback!

I don't think keyed updates is going to prohibit usage on forms (eg. check this example). Do you have a particular case in mind?

Though you are right, that there are going to be some issues when re-ordering. For instance, if you re-order the same component, the properties will re-order but the states won't (eg. check this example). I am going to check for possible solutions within 512 bytes ๐Ÿ˜„

This library does a side-by-side comparison with the VDOM and the DOM and applies the differences. This means that there is never going to be a re-ordering on the DOM level. If there is something changed in a series nodes (eg. if you push an element on the top of a VDOM array) it will apply the changes to the DOM properties, to the entire series and just create a new DOM element at the tail.

So here's an example:

https://codepen.io/mindplay-dk/pen/KJrjLB

I know this one's a bit "odd", but you can imagine a real-world example with a form with multiple rows of inputs... like, say, an expense form, where you enter a description and cost for each expense, and you can add/remove items to the list.

In another real-world example, that silly clock is just a simple spinner indicating an auto-save operation. In that case, sure, I can just show/hide the spinner with style or a class, but then the library is dictating constraints on the kind of changes you can or cannot make to the DOM structure, which means it isn't really a fully generic DOM patcher.

Which might be totally okay for some people :-)

But to be honest, my interest in this library is your beautiful, simple, elegant approach to state in functional components. I fucking love it! ๐Ÿ˜ But couldn't really care less if it's 512 bytes - a typical IP network packet is around 1400 bytes, so...

Do you know this library? It's around 1 KB. It has the most accurate and correct DOM updates of any library of this size. It has brilliant life-cycle management with keyed updates that allows for deferred removals, for things like animation. But it doesn't have stateful components ๐Ÿ˜ข

There are many small libraries in the 1-2K range, but I haven't found one with accurate DOM updates. None of the "build your own React" tutorials make it all the way there.

I know I'm probably asking too much of a 500 byte library ๐Ÿ˜‰

https://codepen.io/mindplay-dk/pen/KJrjLB

Oh, ok, I see your point :)

But couldn't really care less if it's 512 bytes - a typical IP network packet is around 1400 bytes, so...

Well, the concept of this library is to use it in embedded projects, where every byte counts. I was aiming to fit an entire website (lib + css + app) in that MTU size (approx 1.5kb), and hence the accompanying libraries had to be painfully small ๐Ÿ‘ผ

There are many small libraries in the 1-2K range, but I haven't found one with accurate DOM updates. None of the "build your own React" tutorials make it all the way there.

Hehe, so true. I actually gave a similar talk once, and that's usually the point it starts to get a bit too technical.

I think I am going to sacrifice a feature that I recently added (the ability to render DOM element as VDOM children) and this would give enough space to fit a bit better reconciliation with the DOM. I am not sure this will give enough space to introduce keyed updates, but I think I could use the references of the VDOM elements themselves as keys. Let me give it a spin...

If you'd like a source of reference, Inferno appears to have really accurate reconciliation:

https://jsfiddle.net/mindplay/4zhe5Lgj/3/

If you open devtools, you can see it's not even using a placeholder node (e.g. an empty text node) for null/false items. It's reusing elements by type, so even though the <input> in my example isn't keyed, it's getting reused - and its order in the DOM doesn't change, so its state is preserved even though it's child index changes, so the focus/selection state is preserved; you can be click-dragging to change the selection of text in the input without interrupting the operation.

The codebase is fully type-hinted and well-documented, so probably a pretty good reference.

...this is quite complex though ๐Ÿ˜

You know, this library probably doesn't need perfect reconciliation.

It's going to be a candidate for really small, self-contained bundles for reasonably simple UI - so it's probably not important if reconciliation isn't perfect, as long as it's functional.

Keyed updates would still be very useful though - stateful components without keys are something of a liability, so if it's at all possible...

Here's another source of reference:

https://github.com/sweetpalma/gooact/blob/master/gooact.js#L60-L73

This is the smallest reconciliation algo with keyed updates that I've seen. I think it's similar in nature to yours? The main difference is it uses a map and assumes unique keys for every child of a given parent - and generates a default key when no key is specified.

I think I have a solution for you @mindplay-dk . The latest version in devel/0.3.1 supports preserving state in the VDOM element, so instead of passing down a key property, just make sure you carry around the vdom instance.

This means you can create a "keyed" assistant function like so:

/**
 * Optimised function for keeping keyed element reference state in state
 */
const Keyed = (state, key, element) =>
  state[key] = state[key] | element;

/**
 * Where you need keyed updates
 */
function MyElement(props, state) {
  return div(
    Keyed(state, "name", input( ... )),
    Keyed(state, "password", input( ... )),
  )
}

R(H(MyElement), document.body)

And there is now a plugin for this particular use case, let me know if it works for you.

This doesn't really give you keyed updates, does it?

It looks like you're just caching the virtual element in the parent's state - which prevents updating any props of those elements; you will generate new input( ... ) nodes, but those will just get discarded after the first render?

It also won't preserve the actual elements during reordering - rather, it will swap their attributes/properties around, which doesn't account for things like focus and selection; a traditional keyed reconciler will actually change the order of the DOM elements.

Changing the type of an element also doesn't seem to be possible with this approach.

Maybe this approach is useful for inspiration?

Make sure every node has a key, so you can treat them all the same - so they all get keys assigned similar to this:

<div>                            KEYS:
  <h2>...</h2>                   "h2#1"
  <div>...</div>                 "div#1"
  <input key="foo">...</input>   "input#foo"
  <div>...</div>                 "div#2"
  <input key="bar">...</input>   "input#bar"
  <input>...</input>             "input#1"
  <div>...</div>                 "div#3"
</div>

E.g. key = node.type + "#" + (attrs.key || (index[node.type] = index[node.type] || 0 + 1), where index is a {} that starts out empty for every range of children being diffed.

When diffing a range of children, put them all in a map - delete keys from it while diffing existing children, and remove any remaining DOM elements after.

If the element type changes, or the user-supplied portion of the key changes, the effective key changes. This provides good element reuse as well.

I suppose it's slightly more complex than what you're doing currently? ๐Ÿ™‚

I did something similar here and this approach is by far the simplest I've seen that supports keys - I got the idea from here.

It looks like you're just caching the virtual element in the parent's state - which prevents updating any props of those elements; you will generate new input( ... ) nodes, but those will just get discarded after the first render?

Kind of. I am caching the state of the virtual element. Props will be properly updated.

It also won't preserve the actual elements during reordering - rather, it will swap their attributes/properties around, which doesn't account for things like focus and selection; a traditional keyed reconciler will actually change the order of the DOM elements.

This is unfortunately true. I created this pen based on your initial problem, and I was trying to find a solution that will make it work, trying to keep-up with the size budget. Eventually, the proposed solution works as expected, but indeed it's not a "real" keyed update : https://codepen.io/wavesoft/pen/ZwmRqJ .

I suppose it's slightly more complex than what you're doing currently? ๐Ÿ™‚

I am perfectly fine with complex. I am mostly worried about the size (since the size is the flagship feature of this library). Whenever I tried to use a key for orchestrating the reconciliation, I was always getting out of the size budget.

In the process I noticed that the VDom instance itself could be used as a potential "key", and hence the keyed plugin solution.

Though I have to say that I like the idea of a key name that includes the node type. I will check if I can make something compact with this ๐Ÿ‘

Maybe reopen this so others can find it? (I couldn't find it myself ๐Ÿ˜‰)

Good point, let's keep this open until there is a proper keyed update solution

So @mindplay-dk , we finally have proper keyed updates in the same compact size ๐Ÿ˜„
Though this time, I am using brotli instead of gzip to reach the goal.

I am going to do a bit more fiddling, but you can check the https://github.com/wavesoft/dot-dom/tree/devel/0.4.0 branch for details.

I also started a documentation page here : https://wavesoft.github.io/dot-dom/

Sweet! ๐Ÿ˜Ž

Could you publish an 0.4.0-b1 release maybe, so I can try it in my sandbox? ๐Ÿ™

It's not very stable yet for a release unfortunately. There are a few little kins I am trying to iron-out.

You can find the minified bundle here btw : https://github.com/wavesoft/dot-dom/blob/devel/0.4.0/dist/dotdom.min.js

And the key property is the k : http://localhost:1313/dot-dom/docs/api/component-functions/#k-key

Sorry, I tried everything I can think of - I could not figure out how to get that to load as a module in either CodeSandbox or StackBlitz... Guess I'll wait for the release.

@wavesoft Consider publishing under a different dist-tag.

  1. Create a branch (or just modify a working copy)
  2. Edit the version key in package.json to something with a hyphen such as 0.4.0-beta.1
  3. Run npm publish --tag beta

This will allow users to run npm install dot-dom@beta to get the beta version but npm install dot-dom will install the latest (currently 0.3.1).

I copy/pasted the minified file so I could give this a try.

I have this simple demo with multiple counters, which demonstrates key-dependent state management and several aspects of reconciliation, that I port between frameworks for comparison:

https://codesandbox.io/s/funny-ptolemy-37wuq

This lets you test adding, removing and sorting stateful keyed components.

I'm happy to say, this works very nicely! ๐Ÿ˜€

That's awesome! ๐ŸŽ‰

I think this was resolved - let's close this.