Solid: RFC: Control Flow Refactor

Created on 3 Jul 2019  路  8Comments  路  Source: ryansolid/solid

Summary

This proposal looks to simplify/remove control flow by leveraging Solid's own Component API. Instead of writing:

<$ each={ state.list }>{ item => item }</$>

You'd write

<For each={( state.list )}>{ item => item}</For>

Or if you didn't want to use control flow you could use functional underpinnings that could apply to more than just Rendering:

map(() => state.list, item => item)

Please leave any feedback below. I'd love to have feedback on any of the pieces I am proposing here from syntax to overall objective.

Motivation

When I initially implemented Control Flow it was for many reasons:

  1. I wanted to use the compiler to mask accessor timing from the end user required for managing reactivity properly.
  2. I could generalize the approach in DOM Expressions to make any change push out to companion libraries ko-jsx and mobx-jsx.
  3. I believed by merging the reactivity code with the DOM updates directly I could more efficiently render the DOM.

Ironically all 3 ended up being sort of mute:

  1. Development of the general Component and Context API gives a lot of this potential without a compiler.
  2. This put a lot more pressure on 3rd party observable libraries to implement the same features as Solid. It ended up being more work than it was worth to maintain when the interest for those other libraries are lower.
  3. I did not notice a substantial (or any) performance gain over doing the work in 1 or 2 passes.

In addition this approach had these additional problems:

  • Typescript hates $. It just can't handle that as an intrinsic element. Typing in general is a lot looser when I have a generic flow element.
  • If anything I just made all the code much more complicated. It was harder to do your own control flow since the tools were not exposed in an easier way.

By changing this anyone will be able to create their own Control Flow or not so we can handle scenarios that today I cannot anticipate. And it should reduce overall library size from the reduction of complexity in the Control Flow themselves.

Proposal

  1. DOM Expressions removes Control Flow from its core but instead uses a generic reconciler for all expressions. That way regardless of how the array of nodes or Fragments are passed in the renderer will be able to handle it. There will be a slight overhead for Fragments now in that their childNodes will need to be normalized like any array of data coming in.
  2. Babel Plugin JSX DOM Expressions will offer the ability to mark component methods as BuiltIns. If the compiler comes across them they will auto import them from the module set by the moduleName configuration option.
  3. Solid createComponent (used behind the scenes in JSX DOM Expressions) will autowrap element children(JSX Components and DOM elements) as dynamic properties. This means they will be lazy evaluated on access. props.children accessor will run the children code.
  4. Solid will ship with a map function reconciler that does memoized map projection. This can be used to map any reactive list to another reactive list that updates whenever the initial map indexes change. The For control flow will use this internally.
  5. Solid will ship with Components for the existing Control Flow (For, Show, Switch, Portal, Suspense, Context.Provide)
  6. There will be a babel-preset-solid that will use Babel Plugin JSX DOM Expressions as preconfigured for Solid. This will include a set of builtIns (For, Show, Switch, Portal, Suspense) and the correct moduleName etc.

Control Flow

Provide will not be a Built In but rather exported on the Context object the way it works in React.

For

<For
  each={( state.list )}
  fallback={( <div>Loading...</div> )}
>{ item => 
  <div>{ item }</div>
}</For>

Show

<Show
  when={( state.count > 0 )}
  fallback={( <div>Loading...</div> )}
>
  <div>My Content</div>
</Show>

Switch

<Switch fallback={( <div>Not Found</div> )}>
  <Match when={( state.route === 'home' )}><Home /></Match>
  <Match when={( state.route === 'settings' )}><Settings /></Match>
</Switch>

Portal

<Portal anchor={document.getElementById('modal')}>
  <div>My Content</div>
</Portal>

Suspense

<Suspense fallback={( <div>Loading...</div> )}>
  <AsyncComponent />
</Suspense>

Drawbacks

There are a couple drawbacks:

  1. $ was tighter syntax lending to less verbose code. You could instantly identify control flow on the page. Without pre-compilation we want separate imports (and works better with TypeScript).
  2. Dynamic/Lazy evaluated attributes will need to be wrapped in parens {( )}. Pre-compilation knew exactly which attributes were dynamic. Now that this has opened up you need to specify.

Adoption

It is a breaking change across all libraries. And will be released as a new minor version (since there is no major version). For the most part this will be a simple code change to make in existing Solid code.

enhancement

Most helpful comment

I've submitted to the JS Framework Benchmark the numbers look promising.
Screen Shot 2019-07-24 at 12 46 42 PM

All 8 comments

In general I'm not a fan of the "Everything is a component" philosophy and when components become more than just pieces of UI.

Disregarding personal preference I think changing from $ to explicitly named tags like For and Switch has a benefit of making initial reasoning of what the pieces of code do a bit quicker.
But it's such a small syntactic change that the benefit mostly comes from the clear namings suggested than anything else.

So my two cents are that if it improves Solid then go for it. 馃憤

The biggest benefit is perhaps this opens up people writing their own components that control downstream templating or not use Components at all. This opens up just using functions again and benefitting from optimized reconciliation. I still believe that for fine grained due to all the wrapping of accessors and deferred evaluation into computed contexts that Components or special directive-like helpers abstracts those details for beginners better since they provide a syntax that avoids that. So Components because of JSX.

Popular fine grained libraries don't let you wrap your own which set a precedence I was following in the name of optimization which didn't completely pan out. I will be performance tuning this approach to ensure we do not take a hit. But these helpers have a lot of functionality baked in for that reason that complicates and bloats unnecessarily for common cases. Components give a homogeneous way to offer different or extended functionality that has minimal impact on the consumer. Whether you use this For component or someone else's ForBetter component it's a simple swap.

I think this looks great! One of my nit-picks about Solid was this kind of weird <$> control flow that has limited TS support and discoverability.

I personally like the React way using .map, ternaries and && since they are simply part of the language but this proposal actually makes me think these control flow components are the way to go. Routers tend to come with a <Switch> component, <Portal> and <Suspense> have to be components anyways so making <For> and <Map> seems logical. Plus you get niceties like a fallback option. Additionally, if this reduces Solid's complexity and file size, that sounds like a win.

It would be great to just use map and ternaries but there are complexities with reactive contexts that make this not the most optimal approach. Mostly since we are making real DOM nodes you can't just careless map the whole list. You need to use a memoized map. Ternaries are fine they just put all children in a tracking context. Also if the ternary is based on a condition, ie. state.count > 5, everytime count updates it will re-evaluate the children. You can solve this by hoisting the children out of the condition, but control flow handles this all for you.

In any case, I'm happy to announce this has now been implemented and released with v0.9.0.

I've submitted to the JS Framework Benchmark the numbers look promising.
Screen Shot 2019-07-24 at 12 46 42 PM

I would love to write:

<for each={ state.list } item={ item }>
  { item }
</for>
<for each={ state.list } item={ { index, value } }>
  { index } : { value }
</for>

@threeid The primary effect of the change in this RFC is control flow is no longer pre-compiled. They are just normal components. In so they cannot support special syntax that knows their behavior. So a syntax like this is unlikely without the possibility of indicating that the property is an argument and a way of indicating argument order. I suspect the resulting syntax might be more verbose and less obvious.

Ok closing this. It's been a month now. So I think the discussion is done on this.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

niklasbuschmann picture niklasbuschmann  路  5Comments

Yakulu picture Yakulu  路  4Comments

kavsingh picture kavsingh  路  4Comments

aguilera51284 picture aguilera51284  路  8Comments

trusktr picture trusktr  路  3Comments