Mithril.js: Extend onbeforeupdate & onbeforeremove diff return logic to other lifecycle methods

Created on 6 Mar 2017  ยท  11Comments  ยท  Source: MithrilJS/mithril.js

The proposal is to extend oninit, oncreate, onupdate & onbeforeupdate to allow false return values to defer the lifecycle for one draw, and Promise return values to defer lifecycle until resolution.

Synopsis

On a surface level, this is a convenience API that allows the author to easily and intuitively:

  • Replicate v0.2's async component behaviour of m.request in controller, allowing the specification of async conditions in oninit which must be resolved before the view and subsequent lifecycle execute - eg fetch initial view data based on input attributes
  • Freeze lifecycle during oncreate, onupdate and/or onbeforeupdate in the same way as onbeforeremove, in order to ensure the DOM can be animated or 3rd party plugins can perform side-effects to the exclusion of view updates or repeat triggers, before yielding control back to Mithril

false return values allow for the same thing without the component determining the control factors - ie the component determines the lifecycle cannot proceed on this draw, but the conditions necessary for proceeding are determined upstream. This makes sense if eg an upstream promise must resolve etc.

Possibility matrix

Method returns immediately subsequently
All methods false Subsequent lifecycle methods do not execute for this loop. The current step in the lifecycle is deemed to have failed, and must be re-executed during the next loop.
Promise vnode lifecycle is frozen until resolution. Rejection is equivalent to a deferred `false` return. Success indicates completion, therefore a successful `oninit` / `oncreate` would not execute again for this vnode's lifetime.
oninit false The vnode instance is set to an empty fragment. The vnode attempts to initialise on the next loop.
Promise The view and oncreate lifecycle methods execute at the appropriate stage.
oncreate false The vnode runs oncreate instead of onupdate on the next loop.

Potential

The behaviour described above is largely useful for conveniently pausing or aborting component lifecycle while / until [1] arbitrary conditions are met. Pertinently, one Chuck expected an easy way to lay down a promise for when to resume local diffing during animation in the same way onbeforeremove did (chat) - but at discrete points within the component's wider lifecycle, instead of the end. As far as everything in this bracket is concerned, the proposal is essentially a shorthand canonical API for what is feasible but would require a lot of verbose state-keeping in author-land. Chuck's particular requirement can be addressed in the following manner making use of current vnode API.

But if oninit can return a Promise, we can open the door to much easier routing design because the router, as consumer and manager of top level components, can bestow many of the benefits of route resolvers on components. oninit in a route component can be used to defer, redirect and reject route resolution, while retaining the benefits of a full complement of lifecycle hooks and shared state between the resolution control interface (oninit) and the view - thus enabling the possibility of passing asynchronously resolved data to the view (something that isn't possible in the current route API without foregoing deferred URI resolution, or manual state management).

Concerns

  1. The current onbeforeupdate hook describes a condition to be met for aborting the current hook, which keeps things stateless. In principle, statelessness is to be encouraged - arguably onbeforeremove is a special case because integrating the 'being removed' logic into models is exceptionally difficult.
  2. It's not evident whether a settled lifecycle promise should call a redraw, and unlike requests and event handlers, there's no obvious place to hack in an opt-out directive. Intuition says a successful promise should redraw โ€” implicitly, the conditions for progress have been met; whereas autoredraw on rejection quickly conjures up expensive infinite failure loops.

Next steps

My intuition is that it'd be much easier and more efficient to model these requirements in the render loop, but it's a contentious subject and non-trivial work so I'm going to take a stab at getting an author-land implementation (excluding the onmatch convergence logic) of an async component factory with a few use cases to prove possibility and surface ergonomics issues.

I'm writing this as a fork with a test suite to nail down expectations for sanity testing and debate.

Enhancement

All 11 comments

Ugh...this is difficult to do at all, much less performantly. The renderer is written with the assumption of synchrony, which makes this much harder than you might realize. In addition, allowing creation/destruction to be async makes it much harder to keep state in order.

BTW, I've been studying for a couple months now on how to properly do that very thing, async batched vdom rendering, for a project I've been working on. Even with a heavily simplified model (classes only, DOM-agnostic), it's not exactly easy to do from an algorithmic standpoint, and there are some not-so-edge cases you have to address for your end users' sanity:

  • Partially created DOM trees: it's highly undesirable to stop rendering part-way when blocked on an animation. You need to keep rendering its siblings and cousins (i.e. nodes that don't depend on it), and add it when you're actually ready, otherwise you've got an odd-looking UI that looks massively broken for at least a few frames.
  • Partially updated DOM trees: you have similar concerns here, too. When you're unloading parts of the tree, you should update all remaining siblings/cousins around it while the current node is being waited on. Otherwise, you're making highly inefficient use of the batching mechanism, and the UI will look incredibly off.

The above means you have to render the deferred nodes' trees as independent subtrees, potentially with multiple nodes. To do this, you have to "render" them initially as if they were view: () => [], so you can track them as slices and fill them in later. One other issue is that you need to defer the resolution callback in case of badly behaving thenables.

@barneycarroll My best advice for you is to fork Mithril and try to implement it yourself first.

having gone through several design iterations of a vdom engine, my intuition is that something like this would be a nightmare to get right. not being able to reason internally about state is a recipe for massive code bloat to handle a termite-mound level of case-specific bugs. either that, or you're internally locking & queuing updates until all async things are resolved. however, since the resolution is up to the user, you're leaving the lib's internal state consistency in the hands of the user - yikes. domvm's willRemove/willUnmount hooks (and i think Mithril's, too) suffer from this fragility due to deferring to app-space resolution, but this is rather unavoidable.

@isiahmeadows ๐Ÿ‘ WRT the fragment hack. That solves the problem of placeholders.

You raise the specters of batching and an asynchronous rendering model โ€” that's horrifically out of scope. The proposal here is about convenient author APIs for lifecycle management within the constraints of the synchronous loop โ€” the requisite changes amount to an extra flag on the vnode object at most. If you're interested in how async mutations break out of the synchronous render loop and handle removal and ordering in current Mithril, check out onbeforeremove, which handles the difficult part.

@leeoniya async inversion of control in an automanaged, largely stateless system, is terrifying in principle, I agree :)

My devil's advocate argument is that this is already happening with the unmount hooks โ€” the footgun is in the wild. We talked about the dangers of heaping seemingly intuitive enhancements on that a while back with the idea that these hooks might desirably trigger on descendants of removed nodes, and it took me a while but I did finally grok how much of a nightmare that would be to implement (let alone debug!).

In practice AFAICT the worst case scenario under this proposal is broken user-supplied promises that never resolve, leading to nodes that never update until they're removed. But the returned promise contract is easy enough to see at a glance โ€” and localised enough in terms of the unexpected results it could through up โ€” that I can't see this being that difficult to resolve for an edge case compared to, say, a long polling request lock on redraw, or a broken route.

Can you share any particularly grievous stories from DOMVM APIs?

@barneycarroll

Can you share any particularly grievous stories from DOMVM APIs?

the public APIs actually turned out rather well - i think due to a good amount of user feedback, iteration and a rather prolonged incubation period pre-1.0. the only major public changes from v1 to v2 was a move from JSONML to hyperscript and removal of the built-in router and ajax/fetch helpers. if you want lessons learned from the internal engine rewrite, that's another topic.

regarding async-related stuff, not having any form of built-in router or autoredraw system has allowed keeping footguns and bugs/surprises to a minimum - just the willRemove & willUnmount hooks.

@leeoniya

this would be a nightmare to get right

Very true. I've spent the last few months just trying to work out a simplified model, just to prepare to crack this nut. Part of why I haven't said much of it is because it's not exactly a trivial concept to model, much less design an implementation for.

@barneycarroll

You raise the specters of batching and an asynchronous rendering model โ€” that's horrifically out of scope.

Not when oninit can return a promise (as per your possibility matrix). That's an async rendering model regardless of how you want to frame it. And I can tell you stopping the world is the last thing you'd want to do when rendering, because it can make for some really bad UX. (Only the top bar of the page rendered while you wait some slow network or filesystem call?)

Edit: promise rejection semantics (bridging the route resolver gap).

@barneycarroll The state of m.render is held in the call stack, and generators are not supported by IE. Getting this to work (restoring m.render at an arbitrary point) would require a substantial rework of the core, if not a full rewrite...

It is far easier for the remove phase because delaying it doesn't get in the way of the rest of the lifecycle...

Also, I don't understand the point of suspending things at the oncreate/onupdate stage. At that point, all views have been built and are live in the DOM. You'd only be blocking subsequent hooks.

At last, in keyed list, the hook are not called in source order: http://jsbin.com/zivomucugo/edit?js,console In some circumstances (that I can't reproduce right now), you can also have (in a keyed list where an item replaces another) onremove that fires after oncreate.

Closing this โ€” proposal is too big to discuss in issue format and we need tangible complex demos to judge merit.

I'm going to potter away on this as a separate codebase if anyone's interested.

@barneycarroll I just realized this as a workaround for oninit at least: use this HOC for objects (other component types are similar). Note that it doesn't cover the case of oncreate and onupdate, and addressing those will be what actually complicates a usable higher order component, unless you're okay with not supporting those use cases. (I'd still advise using something like the below as a fast-path for objects.)

function async(C) {
    var D = Object.create(C)
    D.oninit = function() {
        var res = Object.getPrototypeOf(this).oninit.apply(this, arguments)
        if (res.then) {
            var old = this.view
            this.view = function () { return [] }
            res.then(function () { this.view = old; m.redraw() })
        }
    }
    return D
}

I don't know about async oncreate and onupdate, though.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ozgurrgul picture ozgurrgul  ยท  3Comments

hadihammurabi picture hadihammurabi  ยท  4Comments

millken picture millken  ยท  4Comments

barneycarroll picture barneycarroll  ยท  3Comments

andraaspar picture andraaspar  ยท  4Comments