mithril.js: Simplify component constructor to function that returns either a view function or a vnode tree

Mithril version:

Platform and OS:

Project:

Is this something you’re interested in implementing yourself?

Description

Depends on #2688 and #2689 - you can see the first because of the two-argument view. It’s easier to explain relative to this.

Replace these idioms:

const Comp = {
    view(vnode, old) {
        return tree
    }
}

function Comp(initial) {
    return {
        view(vnode, old) {
            return tree
        }
    }
}

With these:

function Comp(attrs, old) {
    return tree
}

function Comp(initial) {
    return (attrs, old) => {
        return tree
    }
}

As the rest of vnode.* properties are subsumed with other vnodes, I’d just provide the attrs themselves instead of full vnodes. (Vnode children would be moved to attrs.children to align with virtually every other framework out there - I don’t see the benefit in us remaining special here.)

Why

  1. It’s simpler to write.
  2. It’s simpler to implement.
  3. It’s lower overhead to invoke.
  4. Not much of a reason, but TS compatibility would be increased with this. If TS isn’t, it’d be a much easier ask to get them to correct the incompatibility.

Possible Implementation

  1. In createNode, change state initialization to just this:
var instance = (0, vnode.tag)(vnode.attrs)
if (typeof instance === "function") {
    vnode.state = instance
    vnode.instance = instance(vnode.attrs)
} else {
    vnode.state = vnode.tag
    vnode.instance = instance
}
// render vnode.instance as usual
  1. In updateNode, invoke (0, vnode.state)(vnode.attrs, old.attrs) instead of vnode.state.view(vnode, old).

Open Questions

  • Should we also kill onInit from m.access(...) in #2689? I say no, because that would make it so users could still have that functionality outside components.

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 4
  • Comments: 25 (24 by maintainers)

Most upvoted comments

@orbitbot Caught this in Gitter:

const Comp = () => {
  const poll = (value, { mithril }) => value === '123' && mithril.request('offplanet', ...)

  return (ctx) => m('input', m.access(({ value }) => poll(value, ctx)), 'somestring')
}

Makes me wonder if it’d be better to structure the API like this:

function Comp(ctx) {
  return ctx => vnode
}

function Comp(ctx) {
  return vnode
}

That’d simplify dispatch a little while still providing all the same benefits. And as ctx holds the same identity in both places, shadowing isn’t actually an issue.


I do have a strong bias towards components as functions as functions naturally have names and this aids tremendously in debuggability. (I’ve already in the wild have had issues debugging object components, and have started to use closure components even for stateless components just to recover some of that, despite it being more boilerplate.) And if someone in the future writes dev tools integration, this would make their life easier, too.

Another con of Option 2 is the dilemma between only providing the attrs getter, or providing a plain object attrs in the returned view function but risking a similar shadowing problem like Option 1.

Sleeping a bit on Option 4, I’m starting to like it. Aside from the benefits you list, i could also inherit from an extendable prototype, allowing users and libraries to extend component behavior in interesting ways.

On the other hand, @StephanHoyer’s example makes me wonder, does mithril need to handle state at all? Could it only worry about component identity and handoff state approaches to the user? Perhaps with a few “canonical” helpers.

Ah right, since both versions are functions, mithril doesn’t know what version your component constructor is until after it runs once, which poses a problem.