jorgebucaran / superfine

Absolutely minimal view layer for building web interfaces
https://git.io/super
MIT License
1.56k stars 78 forks source link

RFC: stateful functional components (aka "hooks") #153

Closed mindplay-dk closed 5 years ago

mindplay-dk commented 5 years ago

React announced support for hooks.

This idea is an interesting alternative to class-based components, offering many of the same benefits (control state, in particular) while composing considerably better than class-based components.

I've argued in the past for some sort of stateful component (because controls have control state that's only relevant while they exist) and this seems like a nice trade-off, providing a means of maintaining state without providing a complete different concept that often forces people to factor back and forth between stateful and stateless components.

Background

The basic idea is illustrated here:

The difference between class-based and hook-based components is illustrated here:

You can watch Dan Abramov give a full presentation here.

Proposal

I'm proposing only state and effect hooks for now, as these cover a lot of ground. (I'd like to explore the possibility of introducing shared state contexts, and possibly effects, at a later time.)

State Hook

What I'm proposing for state management is slightly different from what the React RFC proposes. It relies on call-order for identity, which seems really brittle - I'm instead proposing that any hook will need to explicitly define in advance the unique states that may be used within a component.

Here is a counter example:

const useCount = createState()

function Counter() {
  const [count, setCount] = useCount(0)

  return (
    <div>
      Count: {count}
      <button onclick={() => setCount(count + 1)}>+</button>
    </div>
  )
}

In this example, useCount = createState() creates a state hook with no initial state, and therefore, the call to the useCount() function must supply the initial value 0.

Creating a pre-initialized state hook would also be supported:

const useCount = createState(() => 0)

function Counter() {
  const [count, setCount] = useCount()
  // ...
}

In this example, useCount = createState(() => 0) creates a state hook with a built-in initialization function, so the call to useCount() requires no initial value.

Implementation Notes

The internal functions likely need to change in several ways.

Most importantly, the aggressive calls to functional components in the h() function need to be deferred, such that a Function is now regarded (in terms of diffing and patching) as a VNode-type (currently called name) on par with e.g. "div" - the actual calls to functional components would need to happen during the diff/patch itself; as late as possible, rather than (as is now) as early as possible.

Since we'll need to track component instances, we'll need to store these instances somewhere. Storing them in DOM elements won't work, since the functional component isn't an element (and doesn't contain elements before we invoke the functional component) so I suggest storing them in a Map, since this allows the use of the state hooks (useCount etc.) themselves as keys.

I'd suggest functional components should support life-cycle callbacks, since keyed updates will become important - if we treat the functional component occurrence like an element in terms of diffing/patching, then key will come into play, since otherwise we can't discern component instances.

Likewise, I'd suggest supporting the full range of life-cycle hooks oncreate, onupdate, onremove and ondestroy for functional component instancse - and since they have no single corresponding DOM element, I'd suggest passing the Map instance to e.g. oncreate instead of an HTML element.

As for where to store these Map instances between renders, I'd suggest the patch() function return a modified VNode-tree with component instance references. Currently it returns the input VNode-tree exactly as provided - it would likely need to inject the Map as e.g. vnode.instance in the returned VNode-tree for subsequent patching/diffing.


I obviously haven't worked out all the details, so these are just some preliminary ideas.

I'd love to attempt this myself, but I've had no luck with this in the past, and I'm pretty sure I'd fail and get frustrated, so for now I'm just posting the idea ;-)

Thoughts?

brodybits commented 5 years ago

I would likely down-vote this kind of a proposal due to the need for some global state. I think global state would be needed to track which trees would need to be re-rendered by change of each state. If I am mistaken, it would be nice to see some proof.

A coupe alternatives I found from React land:

jorgebucaran commented 5 years ago

@mindplay-dk Sorted by less abstract to more abstract: Superfine → React → Hyperapp

Superfine is a low-level library. It is only the view layer whereas React is a view layer and a primitive state container, usually enhanced through more advanced state containers (managers) like Redux, Mobx, and so on.

Do Hooks mix with React? Yeah. Do Hooks mix with Superfine? Not really. There's not even a concept of state in Superfine.

Now, should we introduce a primitive state container to Superfine? I don't think so. I'm already working on a JavaScript framework that has opinions about views and state management and I'd prefer to keep Superfine as minimal as possible.

Hyperapp+Hooks? That's another issue. 😉

mindplay-dk commented 5 years ago

I would likely down-vote this kind of a proposal due to the need for some global state

There's no need for global state - the patch/diff algo would need to internally track the current node as illustrated by this tweet, but it's not global state, it's local to the calling context and vaporizes after the call.

Superfine is a low-level library.

@jorgebucaran yeah, that's the response I was expecting. I guess, yeah, something like this probably belongs in a fork and not in the library itself. I keep wishing the library could actually be used as a library to experiment with a layer for something like this built over it, but, yeah....

k1r0s commented 5 years ago

im a newcomer but I guess, as @mindplay-dk said, this is quite easy to do through patch method, I agree that this feature os out of scope at this lib

k1r0s commented 5 years ago

for instance https://gist.github.com/k1r0s/be41597d9dbe3a8a4ebcaa1131f6a29b