hyperapp: Can't use keys on custom components.

For now if you try to specify a key for JSX component it won’t be actually used by hyperapp’s diff/patch algorithm:

const state = {
  posts: [
    'Hello',
    'world'
  ]
}

const actions = {}

const Post = (props, children) => <h1>{children}</h1>

const view = (state, actions) => (
  <main>
    {state.posts.map(post =>
      <Post key={post}>{post}</Post> // <= the `key` prop is not used by diff algorithm
    )}
  </main>
)

app(state, actions, view, document.body)

console.log(view(state, actions)) // =>
// { name: 'main', props: {}, children: [
//   { name: 'h1', props: { no key here }, children: ['Hello'] },
//   { name: 'h1', props: { no key here }, children: ['World'] }
// ] }

Demo: https://codepen.io/frenzzy/pen/dJmEOm?editors=0010

Let’s discuss, should we automatically pass the key down into actual virtual DOM node or user must do it manually? How not to forget to do it?

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 22 (21 by maintainers)

Most upvoted comments

@frenzzy You can easily achieve this behavior, by defining your own “higher order” version of the h function, such as like this:

import {h as _h, app} from 'hyperapp'

const h = (tag, props, children) => {
    if (typeof tag === 'function') {
        const node = tag(props, children)
        node.props.key = props.key
        return node
    } else {
        return _h(tag, props, children)
    }
}

You could augment that to also pass along lifecycle events but you’d have to take care to compose them with any lifecycle events the component itself might define on the root node


Edit: On closer reading, maybe the above was already clear. Now we’re discussing wether to add this behavior into the actual h, is that right?

Sounds like @frenzzy is asking to pass down the key property automatically.

This is as far as I know a default behavior in Vue. All properties given to a component are passed down to the root child (a component must have a root child == no component that return an array of children).

But this behavior is not really hard to get in userland. I personnaly took the habit to pass down all properties.

const Component = (props) => (
  <section {...props}>
    <h1>Hi.</h1>
  </section>
)

This basically allow Component user to set class property when it is needed and the class will be set on the root child of the component.

const view= (state, actions) => (
  <main>
    <Component class="bg-red"/>
  </main>
)

Here’s a more advanced (but completely untested) version that should handle class as well as lifecycle events in addition to key


const {h: _h, app} = hyperapp

const composeHandlers = (f1, f2) => (f1 || f2) ? (el, done) => {
    f1 && f1(el, done)
    f2 && f2(el, done)
} : undefined

const composeClassNames = (c1, c2) => (c1 || c2) ? c1 + c2 : undefined

const passOnProps = (source, target) => {
    target.key = source.key
    ['oncreate', 'onupdate', 'onremove', 'ondestroy'].forEach(n => {
        target[n] = composeHandlers(source[n], target[n])
    })
    target.class = composeClassNames(source.class, target.class)
}

const h = (tag, props, children) => {
    if (typeof tag === 'function') {
        const node = tag(props, children)
        passOnProps(props, node.props)
        return node
    } else {
        return _h(tag, props, children)
    }
}


Yes, this is solvable via higher order function. Thanks!

oncreate could be called with element: null because as you mentioned fragment does not have DOM representation.

@infinnie good point about arrays. If node is an array I think the appropriate thing is to just return the node and skip the whole passOnProps thing. At least, setting the same key on all nodes in the array is definitely not the right thing 😉

@frenzzy 🤔 Interesting thought … but what would that mean? I mean… a fragment corresponds to a range of elements, not a single element. So what does oncreate mean for a fragment? And what element do we pass to it?