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?
@frenzzy The key is only used during diffing, what do you mean?
Yes, to make updates effective (diff and patch DOM faster) you must use key. The issue is about an ability to use key for <UpperCasedJSXTags> rather then for <lowerCasedJSXTags> only. For example:
<lowerCased key="used" />
<UpperCased key="used" />
<UpperCased2 key="ignored" />
const UpperCased = props => <tag key={props.key} /> // key passed down manually
const UpperCased2 = props => <tag /> // key loosed
Do we really need to pass key prop down manually, or we can/have to automate it?
Use a different property name instead?
@frenzzy I am going to need some help from someone here to understand what are you talking about haha. I think I've never been this confused before.
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>
)
Say, for rearranging and animating list items, you want to get each list item component’s key like:
<ListItem key={props.ID} content={props.value} />
However, you feel that it is redundant work to pass the key (all the way or not) down to its child element. You still want your rearranging them to properly animate using a method similar to what’s described in https://medium.com/developers-writing/animating-the-unanimatable-1346a5aab3cd
But that expected rearranging you could listen for just does not happen.
I understand the feeling. I would love to see lifecycle event on Component to.
Using the spread props technic you are going to be able to use them too.
This use a simple statement : component are strongly linked to their root element (this one exist only if the component exist) that mean lifecycle events on the root element are equal to lifecycle events on the component.
@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?
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)
}
}
Remember also that we still have components that return arrays. We would need a mechanism to process keys and pass them down to array elements.
Maybe for components which returns an array, h must create a JSX Fragment (VNode with empty tag) automatically to be able to use lifecycle events too.
@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?
oncreate could be called with element: null because as you mentioned fragment does not have DOM representation.
Why not pass the key or resources used to generate the key to the component and leave it up to the implementation of the component to do it? I think I would prefer that.
@frenzzy add it to the h1 of the Post component
const Post = ({ someid }, children) => <h1 key={someid}>{children}</h1>
const view = (state, actions) => (
<main>
{state.posts.map(post =>
<Post someid={post}>{post}</Post> // <= the `key` prop is not used by diff algorithm
)}
</main>
)
Why not pass the key or resources used to generate the key to the component and leave it up to the implementation of the component to do it? I think I would prefer that.
Because this is a manual work, you need to do so for all components, why not automate it?
@frenzzy Could you suggest how you think this could be implemented in core? How would you do it?
A possible implementation will add extra bytes and logic with more computations to the core (see examples above), which is not desirable IMO. Now I think it's better to detect misusage by development build #417 and just show warnings to a developer.
@frenzzy I was just curious how would you apply the key to the component. 🤔
Would this be easier to implement now because we are storing key in the vnode? → https://github.com/hyperapp/hyperapp/commit/9222f99f3de604397cfe9a3580fb8f30c1331947
yes, a bit easier :)
https://github.com/hyperapp/hyperapp/blob/1.1.1/src/index.js#L19-L26
export function h(nodeName, attributes = {} /*, ...rest*/) {
var key = attributes.key || ''
// ...
if (typeof nodeName === "function") {
node = nodeName(attributes, children)
if (node.pop /* Array? */) {
for (length = node.length; length--; ) {
node[length].key += key
}
} else {
node.key += key
}
return node
}
return {
nodeName,
attributes,
children,
key
}
}
Thanks, @frenzzy! I am going to close this as wontfix. If you need this feature can you use a HOA?
Yes, this is solvable via higher order function. Thanks!
Most helpful comment
@frenzzy You can easily achieve this behavior, by defining your own "higher order" version of the
hfunction, such as like this: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?