dustinspecker / dscript

Framework agnostic hyperscript
MIT License
35 stars 2 forks source link

Discussion: Framework Agnostic Stateless Function Components #5

Open dustinspecker opened 8 years ago

dustinspecker commented 8 years ago

Just some brainstorming, but I happily welcome all input.

dscript is currently a framework agnostic hyperscript implementation.

A few folks are already thinking about how it can be used to create framework agnostic stateless components, which sounds awesome.

Unfortunately, there is not a standard for args passed to a render method in components. For example, React's function components are passed a props object, while Deku's function components are passed a model object with a props property.

Proposing the idea of making mapModelToReact/mapModelToDeku/etc to handle this conversion so maintainers can create framework agnostic components.

I'm leaning towards something like:

// framework agnostic component
import dscript from 'dscript'

export default (renderer, mapModelToFramework) => model => {
  // maintainer wants to develop as if it were React
  const props = mapModelToFramework(model)
  // maintainer wants to develop as if it were Deku
  const {attributes, children, props} = mapModelToFramework(model)

  const {div, li, ul} = dscript(renderer)
  return (
    div('.container', [
      ul(
        props.items.map(item =>
          li([item.name])
        )
      )
    ])
  )
}

My main concern is this gets very hairy for consumers of framework agnostic components.

import {createElement} from 'react'
import {mapModelToReact} from 'dscript-model-mapper'
import originalComponent from 'framework-agnostic-component'

const myFrameworkComponent = originalComponent(createElement, mapModelToReact)

export default myFrameworkComponent

We can move most of the boilerplate to other modules, but I think it will still feel awkward for consumers.

EDIT:

I believe this can all be simplified to:

// framework agnostic component
import dscript from 'dscript'
import {mapModelToDeku, mapModelToReact} from 'dscript-model-mapper'

export default renderer => model => {
  // maintainer wants to develop as if it were React
  const props = mapModelToReact(model)
  // maintainer wants to develop as if it were Deku
  const {attributes, children, props} = mapModelToDeku(model)

  const {div, li, ul} = dscript(renderer)
  return (
    div('.container', [
      ul(
        props.items.map(item =>
          li([item.name])
        )
      )
    ])
  )
}
import {createElement} from 'react'
import originalComponent from 'framework-agnostic-component'

const myFrameworkComponent = originalComponent(createElement)

export default myFrameworkComponent

I think only the originalComponent should need to map the model and this starts to look better for consumers already.

lhorie commented 8 years ago

Hi, Mithril author here.

I'm also interested in a way to create compatibility between libraries. Here's a compilation of current challenges that I'm aware of (other than the component argument passing issue you mentioned).

In light of those challenges, I think currently the most feasible technical solution is having a transpiler for each target library. However, this still creates a lowest common denominator limitation, which kinda beats half of the point.

I don't believe a runtime hyperscript wrapper is really the way to go since it adds more syntax fragmentation and doesn't really address the existing fragmentation issues outlined above (let alone addressing them in a performance-conscious way)

dustinspecker commented 8 years ago

Thank you for sharing those concerns. It's greatly appreciated.

I should mention that the scope with this is to only target JSX supported languages for stateless function components. I think there's enough of a standard for JSX to make this much narrower scope plausible. I could not even begin to imagine the effort to support non-JSX supported frameworks or stateful components as you have mentioned.

I'm pretty new to JSX, so I could definitely be misunderstanding the scope of this problem.

Thank you again for the response.

lhorie commented 8 years ago

JSX can actually target a lot of libraries, if not all of them. Snabbdom, for example, can be targeted w/ this, Mithril w/ this, not to mention jsx pragma. Even Inferno, that has a mind-boggling javascript representation, can be targeted by JSX.

To be clear, the fragmentation challenges above are not affected by whether something supports JSX or not (out-of-the-box, or otherwise). The problem is that doing anything even slightly more complex than a hello world will quickly run into one or more of those issues regardless of whether you use JSX, jsx-pragma-friendly hyperscript or a custom flavor of hyperscript, even if you narrow the scope down to stateless components only (this would actually be true even if the scope was to have no component support at all).

dustinspecker commented 8 years ago

Thank you for clearing a lot of stuff up! I really appreciate it.

I definitely underestimated the complication here. I assumed consumers of JSX all took the same input and then mutated it from there to support their framework's needs. Previously, I thought it would be as easy as passing tag, attributes, and children to the JSX pragma; didn't realize how much mutation can occur from JSX implementations before it even hits the createElement.

leeoniya commented 8 years ago

@barneycarroll relevant WRT https://github.com/barneycarroll/plasm

barneycarroll commented 8 years ago

Thanks for the ping @leeoniya. This is a noble task @dustinspecker! My idea for Plasm was chiefly motivated by a desire to see how @joelrich's Cito might work in practical application development.

Whereas Inferno's principal innovation IMO is to harness the efficiency of change detection without relying on an opinionated model API, Cito offers a very comprehensive virtual DOM model that's capable of memoizing DOM fragments. In practice it's impossible to conceive of how this innovation might pan out for a credible application without stateful components (unless you tell the user they have to address component statefulness externally), so I set about writing a framework-agnostic function to provide an intuitive cache layer for higher order functions (the pun at the heart of it is that citoplasm is necessary to bind together the atomic components of intelligent life).

AFAIC the idea of pure components is a step backwards: it depends upon some external caching mechanism — most famously the Flux pattern — whereby you rely on passing in or independently importing an API which contains your entire view-centric model, strongly coupled to the demands of your components: to my mind, one of the revolutionary aspects of the functional virtual DOM paradigm is the ability to write Turing-complete views which can query and transform arbitrary data to map to the concerns of user interfaces in the places where those concerns are encountered — this is in stark contrast to the populist idea that this task should be handled centrally by the back-end API or a heavy front-end controller or 'view model'. Because I'd already found certain assumptions in Mithril & React to be unhelpful for real world application development — namely the idea that a component is bound to a single unique live DOM root node — I wanted Plasm to make as few assumptions as possible. Statelessness, DOM-centric caching logic, reliance on externally defined APIs, are all liabilities that limit the application of the tool and defer crucial application concerns to third party techniques.

So my guiding principles in building a generic component API are to provide a caching mechanism that doesn't depends upon external APIs and to allow an intuitive external & internal signature which exposes the minimum API necessary. The goal is less about interoperability in a React-dominated culture, more about separating the concerns of stateful componentisation from virtual DOM API. So Plasm consists of a single function, exposes no other functions internally, and returns whatever it is your virtual DOM framework needs to render.