jorgebucaran / superfine

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

Preliminary Fragment Tag for Superfine 6.0.0 #141

Closed rbiggs closed 6 years ago

rbiggs commented 6 years ago

I forked Superfine and created a branch so I could give a shot at adding support for React-style Fragment tag. I've got a preliminary Fragment tag that can be inserted into the DOM, even document.body, or any other container element. You can also update the Fragment's contents. Here's the branch: https://github.com/rbiggs/superfine/tree/Fragment-Support.

And here's a working Codepen: https://codepen.io/rbiggs/pen/GGVzRY?editors=0010

If you scroll down to the bottom of the README you'll see the documentation for how to use the Fragment tag.

This branch includes a new tag: Fragment that creates a new VNode type: FRAGMENT. I also had to update both the createElement and removeElement functions to work with the Fragment VNode.

What's Missing

If you try out the example on Codepen, you'll notice that when you update the Fragment tag, all of its children are rerendered instead of only the changed child. I'm not exactly sure how to check whether a Fragment's children have changed. Any suggestions from those more familiar with the inner works of Superfine are greatly appreciated. @jorgebucaran, do you have any idea how to resolve that? With React 16, only the Fragment child that needs to be updated gets updated.

TODO: The current tests would need to be updated for these changes, particularly those made to createElement and removeElement.

jorgebucaran commented 6 years ago

@rbiggs Both Superfine and Hyperapp V2 components are eagerly evaluated, thus supporting components that return arrays. Fragment could be implemented by simply returning its children and ignoring its props. The downside is that it doesn't work for the root of your application.

So, the reason you want proper DocumentFragment support is to just be able to avoid a root element?

mindplay-dk commented 6 years ago

Please see my related notes here. (scroll back for more context.)

I think this could remove the need for fragments entirely - all components (functional or possibly some kind of stateful component in the future) would essentially expand in-place. Implementing a functional component Fragment would then just be:

const Fragment = props => props.children

I'm pretty sure the real root of the problem is the aggressive evaluation of functional components in the h function - at the moment, there's a forced 1:1 parent/child-relationship for functional components, and also, the "real" structure of child VNodes isn't visible to parent components, because any functional child-components have already been evaluated before a functional parent-component executes.

rbiggs commented 6 years ago

Yup, I'm aware of that for Hyperapp and Superfine. You could just make a simple tag like this:

function Fragment(props) {
  return props.children
}

But this type of simple Fragment tag must be consumed by another component. You can't just render a Fragment tag directly into the DOM. With React you can render a Fragment directly into any container. Notice the difference between these two examples, React first, followed by Superfine:

/////////////////
// React version
// Notice that we can render it directly to the DOM:
/////////////////
function List(props) {
  function update() {
    list = render(<List data='New Stuff Here' />, document.body)
  }
  return (
    <React.Fragment>
      <p>One <input type='checkbox' /></p>
      <p>Two</p>
      <p>{props.data}</p>
      <p>
        <button onClick={() => update()}>Change</button>
      </p>
    </React.Fragment>
  )
}

ReactDOM.render(<List data='Three'/>, document.body)

/////////////////////
// Superfine version 
// Notice the extra wrapper function needed to consume the Fragment:
/////////////////////
function Fragment(props) {
  return props.children
}

function List(props) {
  function update() {
    list = patch(list, <Container data='New Stuff Here' />, document.body)
  }
  return (
    <Fragment>
      <p>One <input type='checkbox' /></p>
      <p>Two</p>
      <p>{props.data}</p>
      <p>
        <button onclick={() => update()}>Change</button>
      </p>
    </Fragment>
  )
}

function Container(props) {
  return (
    <div>
      <List data={props.data} />
    </div>
  )
}

let list = patch(null, <Container data='Three'/>, document.body)

Fragments for Hydration

Now let's say you want to hydrate some server rendered content that happens to be a defintion list (DL). You've written a tag that returns the new list items:

function 
function Glossary(props) {
  return (
    {
      props.items.map(item => (
        // Without the `key`, React will fire a key warning
        <Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </Fragment>
      ))
    }
  );
}
ReactDOM.render(<Rows items={newData}/>, document.querySelector('dl'))

The above example can easily hydrate a definition list with new entries. If we try to do that with Superfine, we'd need to wrap the dt and dd in another tag, breaking the definition list markup. No way around that. Hydrating existing tables could have similar problems. Now if you're a full stack developer, you can go and change the server rendered markup. If you're not, you've got a problem.

That said, I've never needed to render a Fragment tag directly into the DOM, so 🤷‍ This was just an attempt to try and reproduce the functionality of React Fragments with the latest Superfine. I though the new VNode flags would make it easier to pull off, which they did. But I'm having difficulty figuring out how to get Fragment children to update efficiently instead of re-render all of them.

Here's the documentation for the Fragment tag

And here's a Codepen of a React Fragment.

@mindplay-dk You point about h function:

the real root of the problem is the aggressive evaluation of functional components in the h function

But I'm not sure how to change that, which forced me to seek other means to pull it off.

jorgebucaran commented 6 years ago

@rbiggs Patching fragments can become messy and I don't think it's worth adding this complexity, at least not now. I have no plans to change how h eagerly evaluates components at the moment, so there is little to gain here, but I am willing to be proven wrong.

rbiggs commented 6 years ago

I know. This was more of an experiment to see how far I could get to have same support as React.Fragment. 🏄‍♂️