Mithril.js: Object.create used in initComponent messes up state

Created on 2 Aug 2017  路  9Comments  路  Source: MithrilJS/mithril.js

I can't explain exactly what's happening, but here is the behaviour I'm experiencing.

I'm using a simple classic constructor:

function MyComponent(arg1,arg2,arg3){
  this.field1 = arg1;
  this.field2 = arg2;
  this.field3 = arg3;
}
MyComponent.prototype.oninit = function(vnode){
  this.vnode = vnode;
};
MyComponent.prototype.getVNode = function(vnode){
  return this.vnode;
};
MyComponent.prototype.view = function(){
  return m('h1', 'Test');
};

I create an instance of the component whenever I need it: var component = new MyComponent(1,2,3);

Then when I need to mount it: m.mount(el, component);

This has been working fine, however I have now noticed some weirdness around the vnode and the this state in Mithril lifecycle functions.

Namely vnode.state === this but vnode.state !== component. Ultimately this means that any properties I set on this in a Mithril lifecycle function is not available for MyComponent calls (in my example above component.getVNode() will always return undefined.

It seems for some reason the object instance I provide to m.mount is passed through a call to Object.create. I'm not sure what the purpose of this is and I wonder if it needs to be gated to not occur in my use-case or if my use-case is incorrect for the way Mithril is designed to work?

For reference, I believe this line to be the culprit:

https://github.com/MithrilJS/mithril.js/blob/157fb7d446ff097f98b1582ada3526d12de6600d/render/render.js#L113 : vnode.state = Object.create(vnode.tag)

Gotcha Question

All 9 comments

this within mithril components is assigned to vnode.state, not to the component itself; I believe your use-case is, in your words, "incorrect for the way MIthril is designed to work".

@klick-barakgall vnode.state is supposed to be the component's instance, rather than the component itself. For example:

  • For object literal components, it's vnode.state = Object.create(Component).
  • For class components, it's vnode.state = new Component(vnode).
  • For closure components, it's vnode.state = Component(vnode).

For convenience, of course, vnode.state === this, but that's just a convenience detail that wasn't even introduced until closer to the initial 1.0 release IIRC (it started out as vnode.tag === this).

Of course, vnode.state === this is meant to make it easier to do some things, but it does in fact make it harder to create higher-order components as class instances. The recommended pattern for creating them is to create a function that returns a component that doesn't depend on this === component:

function makeComponent(field1, field2, field3) {
    // do whatever *global* initialization you need
    return {
        oninit: function (vnode) {
            // do whatever *instance* initialization you need
        },
        view: function (vnode) { ... },
        // other component methods as applicable
    }
}

var Component = makeComponent(1, 2, 3)
// ...

In case you're curious, we tend to prefer functions and record-like objects over classes when dealing with data (such as vnodes), since it's easier to deal with things when you have less state to deal with.

Thank you @CreaturesInUnitards for the quick response.

I was under the impression vnode.state would point to the component instance I had passed in so I could inspect and manage my state between Mithril lifecycle events and my component logic, and also having the benefit that this would refer to the same object.

Is there a reason this is not the case? In general when you need to check a value or update it from the component how is this done? Or is the problem that I'm using a constructed instance object? What should I be using instead?

Regardless, as a workaround I now have my constructor function adding a this._me = this;, which then gets translated into the prototype of the vnode.state object. Now I can use var _this = this._me; in any component function to make sure I have a way to communicate back to the component.

@klick-barakgall

Is there a reason this is not the case?

Do check out my last comment, which said this, among other useful things you might want to consider:

For convenience, of course, vnode.state === this [...]

It's easier to type out this.foo instead of vnode.state.foo, and in highly nested vnode trees, that little bit can make a pretty significant difference. Also, this behavior is documented here.

Somehow I missed your response @isiahmeadows, thank you for clearing all of that up.

In your example however you mention for class components it's new Component(vnode) but I want to construct the object myself. So I think I'm getting a hybrid of object literals and class components, is that right?

Because I do have a class component instance that I pass down, but it's being treated like an object literal since I'm not passing the constructor. I almost feel like if tag.constructor is not falsy that it should just take my object as the tag instance. I dunno if I'm missing something that would make that not make sense..

Additionally I still have to wonder why Object.create(tag) is used where it seems to me the intention is closer to Object.assign({}, tag);

Your example contains zero state. So a simple view function would work too

function viewFn(arg1, arg2, arg3) {
  // build stuff from the arguments
}
m.mount(el, { view: () => viewFn(arg1, arg2, arg3) });

However, if you need a stateful component you can easily do that without changing the API of viewFn:

const component = {
  oninit: vnode => {
    vnode.state.stateVar1 = vnode.attrs.arg1 + vnode.attrs.arg2
  }
  view: vnode => {
    // do something with vnode.state and vnode.attrs
  }
}

function viewFn(arg1, arg2, arg3) {
  return m(compontent, { arg1, arg2, arg3 })
}
m.mount(el, { view: () => viewFn(arg1, arg2, arg3) });

I tend to not expose any components but view functions that use them. Than you are free with the arguments and you can easily use fp stuff like currying and alike. Also you are free to switch back to a plain view function if you don't need state anymore.

I also never use prototype and this.

@klick-barakgall

In your example however you mention for class components it's new Component(vnode) but I want to construct the object myself. So I think I'm getting a hybrid of object literals and class components, is that right?

You could do that. You could similarly return a class out of it, if that helps you understand this better:

function makeComponent(field1, field2, field3) {
    // do whatever *global* initialization you need
    return class Component {
        constructor(vnode) {
            // do whatever *instance* initialization you need
        }
        view() { ... }
        // other component methods as applicable
    }
}

var Component = makeComponent(1, 2, 3)
// ...

@klick-barakgall

Additionally I still have to wonder why Object.create(tag) is used where it seems to me the intention is closer to Object.assign({}, tag);

The difference really boils down to this:

  • Object.create(tag) inherits all the properties and methods from the literal object tag.
  • Object.assign({}, tag) copies all the properties and methods from tag.

It really comes down to understanding prototypal inheritance. Object.create(object) creates a prototypal clone which inherits from the object itself similarly to class inheritance. Object.assign({}, object) creates a new object and copies all of the the own (not inherited) properties of the other object to it.

For instance, Object.assign({}, object) is this:

var base = Object.assign({}, parent)

var base = {}
for (var key in parent) if ({}.hasOwnProperty.call(parent, key)) {
    base[key] = parent[key]
}

Conversely, Object.create is pretty much this (and it's why we use it):

var base = Object.create(parent)

var base = {}
base.__proto__ = parent

I'd invite you to look up "JavaScript prototypes" and "JavaScript inheritance", and check out these three articles to help get you started. Mithril leverages some of the less well-known parts of JavaScript, such as prototypal inheritance (component creation), this re-binding (lifecycle hooks), and closures (streams, event handlers), so those will help you in understanding how to use it much better and even how it does certain things.

Thanks so much for all the references and support. I do believe I am simply using it the wrong way. I'm considering restructuring my code if possible to just use view functions.

Thanks again, to everyone!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

barneycarroll picture barneycarroll  路  3Comments

omenking picture omenking  路  3Comments

marciomunhoz picture marciomunhoz  路  4Comments

StephanHoyer picture StephanHoyer  路  4Comments

simov picture simov  路  4Comments