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

Extend (present, previous) signature to all component methods #2098

Closed
barneycarroll opened this issue Mar 3, 2018 · 2 comments
Closed

Extend (present, previous) signature to all component methods #2098

barneycarroll opened this issue Mar 3, 2018 · 2 comments
Labels
Type: Enhancement For any feature request or suggestion that isn't a bug fix

Comments

@barneycarroll
Copy link
Member

I wanted to bring this up for discussion while public vnode-exposing API changes were fresh in people's minds off the back of #1744.

The proposal is to have view and all other lifecycle methods match the onbeforeremove (OBR) signature.

This would in theory allow some interesting opportunities:

  • view could subsume OBR behaviour behaviour by returning the old vnode or its instance, allowing for a less object-oriented approach
  • views and updates can perform transitions between attribute states
  • oninit / oncreate can perform incoming logic depending on the previous node in place

I haven't looked into feasibility.

Any thoughts?

@pygy
Copy link
Member

pygy commented Mar 3, 2018

That's probably feasible.

Regarding component methods signature, we may also revisit the possibility of optionally returning a Promise for suspending a given phase as you suggested previously.

React will support throwing Promises to that end, I hope we can manage that situation with just returning values.

Edit: removed off-topic idea.

@barneycarroll
Copy link
Member Author

we may also revisit the possibility of optionally returning a Promise

@pygy I'm glad you bring this up. I've been exploring various component extension patterns for a while and I believe the practical possibilities of asynchronous lifecycle methods can be better exposed without implementing that proposal. I'd like to submit this proposal as an alternative to that one, because, with hindsight,

Optionally promise-returning lifecycle methods are bad

I started working on a proof of concept for 'temporal components' about 6 months ago, but I've found it difficult to develop & expound on the idea because despite the nominal intuitive convenience of returning a promise for a 'blocking' oninit (eg one that fetches data that the view cannot execute without) or an onupdate (that manually mutates the DOM in a way that view re-execution might intrude on), in the bigger picture, these mechanisms become incredibly hard to reason about.

To wit:

  • Inversion of control conflicts: should a deferred oninit or onupdate block the execution of onbeforeremove (OBR)? OBR offers a very specific clause for inverting control of vdom tree patching from a higher order to the lower order, but as discussed in Proposal: invoke and await nested onbeforeremoves #1182 & onbeforeremove does not always trigger when navigating away #2090, seemingly intuitive attempts at procedurally extending that kind of behaviour quickly raise more ambiguities than they resolve.
  • Lifecycle composition (even if it isn't a widespread mechanism in the community at present) becomes drastically more complicated if we must continuously fork logic to account for the fact that any given method's synchronicity is ambiguous and determinable only after its execution. There's an excellent article somewhere (I can't find it at the moment) illustrating how pernicious 'potentially async' APIs become at scale: in short the complexity can never be isolated and spreads throughout the call graph in ways that are even more complicated to accommodate than null checks.

Allowing lifecycle methods to be potentially asynchronous exponentially increases the complexity of Mithril code interoperability and raises all sorts of edge cases where previously predictable assumptions give rise to devilish ambiguity. I recant the suggestion and am happy to discuss my strong opposition to it in any given context.

But, as I intimated earlier, all the functionality it exposes can be achieved with a little more elbow grease in safer ways if we simply explode & expose the state of salient Promises. By chaining off the relevant promises and binding vnode.state flags for its various states along with a finally(m.redraw), we can keep methods synchronous, conditionally forwarding on to other methods if necessary (an onbeforeupdate (OBU) can, for example, ascertain a crucial initialisation promise rejection and re-invoke oninit, conditionally returning false).

Binary signature lifecycle methods enable safer & greater power in the same domain

The alluring convenience in allowing any given method to return a promise — over the pattern described above — is that we are able to avoid some the object-oriented drudgery of juggling state properties and call forwarding by grouping otherwise disparate execution contexts into an overarching scope. In this respect it's similar to function components. Crucially, this is where this proposal shines — and like View components, it's an area where Mithril v1's judicious early design decisions allows us to leapfrog the patterns React is just beginning to explore.

The killer straw man is one in which thanks to this signature extension, we are able to subsume oninit, oncreate, onbeforeupdate and onupdate into the view method:

const AllInOne = {
  view: (now, then) => {
    if(!then || then.tag != now.tag){
      // oninit:
      now.state.x = y

      requestAnimationFrame(() => {
         // oncreate:
         now.dom.animate(/**/)
      })
    }
    
    else if(now.attrs.x === then.attrs.x)
      // onbeforeupdate:
      return then.instance
    
    else 
      requestAnimationFrame(() => {
        // onupdate:
        now.dom.animate(/**/)
      })
    
    return /* view */
  }
}

By effectively turning the view method into a stream scan (albeit with reversed signature order), we're able to concatenate a multitude of concerns into a cross-referencing scope, with previousVnode.instance being equivalent to stream.HALT (or OBU's false), and we leave asynchrony out of the vdom API itself. Statement-phobics like myself can abstract the various conditional queries into functors wrapping the view, and functional enthusiasts (cc @JAForbes) can of course use function components to lessen the burden of vnode as a stateful object.

Nonetheless, I think this pattern lends itself especially well to the original POJO component convention, especially in combination with the View component pattern, inasmuch as discrete concerns can be abstracted via higher order View components*: By exposing the previous vnode at every stage, and enabling lower order components to interface with higher order components' state objects, we drastically increase the potential knowledge & expressive power of any given virtual DOM expression while keeping the underlying mechanisms relatively simple and predictable.

Asynchronous IoC in the virtual DOM tree

This doesn't address React's promise-rejection-within-a-component-as-current-draw-cancelling-&-deferral-mechanism specifically. What I want to impress with the above is that thanks to Mithril v1's ingenious vnode API, with lifecycle mechanisms written with the benefit of hindsight on Reacts, we have (with a relatively small tweak) the power to subsume the practical concerns of their mechanism in a holistically better way. I demonstrated in #1182 how View components enable us to explicitly specify async inversion of control concerns that enable lower order components to influence higher order virtual DOM tree concerns.

Using a similar mechanism to the above, a mounted Mithril application can enable (even without the extension under proposal) higher order nodes to expose mechanisms which lower order nodes can invoke to influence root-level draw concerns. In the case of more indirect apps, @JAForbes has workable concepts for how rendered apps, using stateless Meiosis-type patterns, can ease the burden of constant reference passing by providing a contextually extended m reference to pure functions, enabling a cleaner practical implementation of React's context API via dependency injection.

New possibilities

So far I've covered how we can address relatively familiar issues, but providing an API that gives access to the previous vnode in place as early as the initial lifecycle offers fantastic opportunities for resolving IoC hell as regards transition between discrete components / elements. We can hereby infer that the new node is an essentially different entity to the previous one in place and determine any manner of complex transition logic there. This also solves one of the key problems in generalising route resolver logic that allows an incoming route to keep the stale view in place while waiting for new initialisation conditions to resolve.


* The View component allows for declarative vdom expressions to instantiate, manipulate & query state between nested virtual DOM entities on an ad-hoc basis by enabling all lifecycle attributes - including view to be declared as attributes:

const View = {
  view(v){ return (
      v.attrs.view
    ?
      v.attrs.view.apply(
        this,
        arguments,
      )
    :
      v.children
  )}
}

@dead-claudia dead-claudia added the Type: Breaking Change For any feature request or suggestion that could reasonably break existing code label Oct 28, 2018
@barneycarroll barneycarroll added Type: Enhancement For any feature request or suggestion that isn't a bug fix and removed Type: Breaking Change For any feature request or suggestion that could reasonably break existing code labels Dec 8, 2018
@dead-claudia dead-claudia moved this to Completed/Declined in Feature requests/Suggestions Sep 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Enhancement For any feature request or suggestion that isn't a bug fix
Projects
Status: Completed/Declined
Development

No branches or pull requests

3 participants