-
-
Notifications
You must be signed in to change notification settings - Fork 926
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Simplify our component model #2295
Comments
This also kind of relates to #2278. |
@isiahmeadows thanks. What's the use case for maintaining |
@barneycarroll That's what would happen. I'm proposing renaming the property so it's clearly not. As for why I'm not doing away with the value itself, we need the instance somewhere for our own purposes, even if it's in something like |
Maybe we could add a deprecation warning in v2 when accessing |
@StephanHoyer 99% of the migration for this could be done before upgrading Mithril, so I'm not convinced it's necessary. If this change were implemented for v3, here's how the migration would go:
The last two steps would entail very little effort compared to the first three, since those would catch 99% of your mistakes. // This is probably broken and it obviously won't work on IE or other ES5 environments.
function m(tag, ...rest) {
if (typeof tag !== "string") {
if (typeof tag === "function" && tag.prototype && tag.prototype.view) {
throw new Error("Class components are no longer supported!")
} else if (typeof tag !== "function" && !tag.$$wrapped) {
tag.$$wrapped = true
const {oninit, onbeforeupdate} = tag
const lock = vnode => {
if (vnode.$$locked != null) return
vnode.$$locked = false
let state = vnode.state
Object.defineProperty(vnode, "state", {
get() {
if (vnode.$$locked) throw new ReferenceError("`vnode.state` is deprecated!")
return state
},
set(s) { state = s },
})
}
;["oninit", "oncreate", "onupdate", "onbeforeremove", "onremove"]
.forEach(name => {
const method = tag[name]
tag[name] = function (vnode) {
lock(vnode)
vnode.$$locked = true
try {
return method.apply(this, arguments)
} finally {
vnode.$$locked = false
}
}
})
tag.onbeforeupdate = function (vnode, old) {
lock(vnode); lock(old)
vnode.$$locked = old.$$locked = true
try {
if (onbeforeupdate) return onbeforeupdate.apply(this, arguments)
} finally {
vnode.$$locked = old.$$locked = false
}
}
}
return oldM(tag, ...rest)
}
} |
I wonder whether we could add a third type of component
That is to say if a closure component returns a function, it's treated as a view. const simpleCounter = (vnode) => {
let count = vnode.attrs.count || 0;
return ()=> m('', [
m('h1', `The count is: ${count}`),
m('button', {onclick: ()=> ++count}, '+'),
m('button', {onclick: ()=> --count}, '-'),
])
} I'm finding that in most cases I use a closure component, I'm just returning an object with a view function. This would mean a change to the if (typeof sentinel === "function") {
if (sentinel.$$reentrantLock$$ != null) return
sentinel.$$reentrantLock$$ = true
const view = sentinel(vnode)
vnode.state = typeof view === 'function' ? {view} : view
} |
I love this idea, if indeed it doesn't have more wide-ranging repercussions |
@nordfjord I don't see anything different between that and this: const simpleCounter = (vnode) => {
let count = vnode.attrs.count || 0;
- return ()=> m('', [
+ return {view: ()=> m('', [
m('h1', `The count is: ${count}`),
m('button', {onclick: ()=> ++count}, '+'),
m('button', {onclick: ()=> --count}, '-'),
- ])
+ ])}
} It's literally 8 extra characters per component, and I'm convinced it's worth the added complexity. (The goal of this proposal is to simplify the model, not to add yet another component type.) |
I'm pretty much on board with this proposal. But to play devil's advocate: Is the lack of class components a liability for adoption? A lot of people think in classes. Typescript TSX types can't yet handle closure components. i.e. Browser vendors seem to be more aggressively optimizing classes over closures. While this is almost never going to be an issue for stateful components, would it ever desirable to have a higher performing stateful component with less overhead than a closure? The thing I am still more attached to is Convenient async oninit: function Comp() {
let item
return {
async oninit(v) {
item = await m.request('/api/item' + v.attrs.id)
},
view() {...}
}
} A pitfall without oninit: function Comp(v) {
const initialFoo = v.attrs.foo
return {
view() {
// Using stale vnode by mistake
return m('ID: ' + v.attrs.id)
}
}
} Alternately, to prevent accidental re-use and to garbage-collect the initial vnode: function Comp(v) {
const initialFoo = v.attrs.foo
v = null
return {
view() {
// Will throw an obvious error
return m('ID: ' + v.attrs.id)
}
}
} Typescript use case for export default function Comp(): m.Component<Attrs> {
let item: Item | undefined
return {
async oninit(v) {
item = await m.request('/api/item/' + v.attrs.id)
},
view(v) {
// ...
}
}
} More verbose and awkward without: const Comp: m.FactoryComponent<Attrs> = function(v) {
let item: Item | undefined
async function init() {
item = await m.request('/api/item/' + v.attrs.id)
v = undefined as any
}
init()
return {
view(v) {
// ...
}
}
}
export default Comp There might also be some use cases for affecting global state in a POJO component's oninit hook rather than in oncreate. |
@isiahmeadows @spacejack I don't see the benefit to ripping |
The main reason why I want to rip it out is because it's just duplicated functionality.
If we rip out the @spacejack You just gave me an idea here: how about we just drop the initial |
Edit: Use previous vnode instead of a new @spacejack @CreaturesInUnitards I just came up with an alternate way that could simplify components further:
There are several benefits of this:
ComparisonTo draw from a couple of React's hooks API examples, here's how this would compare: // v2
function FriendWithStatusCounter() {
let count = 0
let isOnline, prevId
function onChange(status) { isOnline = status.isOnline; m.redraw() }
return {
oncreate({attrs}) {
document.title = `You clicked ${count} times`
ChatAPI.subscribeToFriendStatus(attrs.friend.id, onChange)
prevId = attrs.friend.id
},
onupdate({attrs}) {
document.title = `You clicked ${count} times`
if (prevId !== attrs.friend.id) {
ChatAPI.unsubscribeFromFriendStatus(prevId, onChange)
ChatAPI.subscribeToFriendStatus(attrs.friend.id, onChange)
prevId = attrs.friend.id
}
},
view: () =>
isOnline === null ? 'Loading...'
: isOnline ? 'Online'
: 'Offline',
}
}
// This proposal
function FriendWithStatusCounter() {
let count = 0
let isOnline
function onChange(status) { isOnline = status.isOnline; m.redraw() }
return {
onupdate({attrs} = {}, {attrs: prev} = {}) {
document.title = `You clicked ${count} times`
// Lodash lets me do optional chaining
if ((attrs && attrs.friend.id) !== (prev && prev.friend.id)) {
if (prev) ChatAPI.unsubscribeFromFriendStatus(prev.friend.id, onChange)
if (attrs) ChatAPI.subscribeToFriendStatus(attrs.friend.id, onChange)
}
},
view: () =>
isOnline === null ? 'Loading...'
: isOnline ? 'Online'
: 'Offline',
}
} If you wanted to use a helper, similar to what React allows with custom hooks, it still composes reasonably well: // v2 + helper
class FriendStatus {
constructor() {
this.prevId = this.isOnline = undefined
this.onChange = status => { this.isOnline = status.isOnline; m.redraw() }
}
update(friend) {
if (this.prevId === friend.id) return
if (this.prevId != null) {
ChatAPI.unsubscribeFromFriendStatus(this.prevId, this.onChange)
}
if (friend.id != null) ChatAPI.subscribeToFriendStatus(friend.id, this.onChange)
this.prevId = friend.id
}
}
function FriendWithStatusCounter() {
const status = new FriendStatus()
let count = 0
return {
oncreate({attrs}) {
document.title = `You clicked ${count} times`
status.update(attrs.friend.id)
},
onupdate({attrs}) {
document.title = `You clicked ${count} times`
status.update(attrs.friend.id)
},
view: () =>
status.isOnline === null ? 'Loading...'
: status.isOnline ? 'Online'
: 'Offline',
}
}
// This proposal + helper
class FriendStatus {
constructor() {
this.isOnline = undefined
this.onChange = status => { this.isOnline = status.isOnline; m.redraw() }
}
update(friend, prev) {
if (prev.id === friend.id) return
if (prev.id != null) ChatAPI.unsubscribeFromFriendStatus(prev.id, this.onChange)
if (friend.id != null) ChatAPI.subscribeToFriendStatus(friend.id, this.onChange)
}
}
function FriendWithStatusCounter() {
const status = new FriendStatus()
let count = 0
return {
onupdate(vnode, prev) {
if (vnode) document.title = `You clicked ${count} times`
status.update(vnode && vnode.attrs.friend, prev && prev.attrs.friend)
},
view: () =>
status.isOnline === null ? 'Loading...'
: status.isOnline ? 'Online'
: 'Offline',
}
} You can see that the helper is also considerably simpler.
As I mentioned tongue-in-cheek on Gitter, TypeScript is already running into problems of their own making from hard-coding the types of React's classes, stateless functional components, and elements into the compiler rather than making them configurable - React 16 changed these types substantially, and TS already (mostly) supports attributes. So I expect TypeScript to resolve their end eventually. (And as slow as Mithril's historically evolved, they would likely beat us.) |
@isiahmeadows when submitting issues consisting of long nested lists, I'd appreciate numbering in order to be able to reference things specifically - otherwise discussing the detail becomes very difficult.
This belongs in user-land. The existing API can easily be adapted to cater for this: const Static = initial => ({
view: () => initial.children,
})
// used as
m(Static,
memoizedContents,
)
// alternatively
m.fragment({
onbeforeupdate: () => false,
},
memoizedContents,
) I'm glad #2098 is getting traction. But as regards changing the behaviour on But it strikes me that we may not need the hook at all. Part of the justification for the magic of React's new hooks rests on React's opinionated draw pipeline. We don't have that burden to satisfy and currently we can guarantee that, when a view is executing, the DOM will be ready on the next tick. This in mind we can fold all eventualities into a 2-hook system {
view: (now, then) => {
if(!then){ // first pass
oninit()
setTimeout(oncreate)
return initialView
}
else { // subsequent passes
onbeforeupdate()
setTimeout(onupdate)
return then.children // <= onbeforeupdate: () => false
return furtherView
}
// universal
setTimeout(onafterview)
return view
},
onbeforeremove: (now, then?) => {
await onbeforemove()
onremove()
},
} |
My bad. It was basically just a giant brain dump. I updated it to be numbered now.
Edit: Now that I read what you were saying correctly, I think you misunderstood what I was saying. That's intended to be a primitive component, down the vein of what was discussed in #2279. I've since updated the suggestion to use the old vnode instead of a special vnode, per your suggestion. (I altered it for reasons explained below.)
Mithril has an almost identical draw pipeline to React's pre-Fibers, and so does nearly every other virtual DOM library. Also, we don't have a reference to the DOM until after the returned tree is rendered, so you can't fuse the I did consider dropping Your |
@isiahmeadows if you destructure the vnode in the arguments, then yes. But if you only query m.mount(document.body, {
view: v => {
setTimeout(() => {
v.dom.style = 'color: red'
})
return m('p', 'Hello!')
}
})
Could you elaborate on this? I think you're referring to the problem of how to trigger exit animations in descendants of a removed node. This can be achieved in user-land.
Well observed - the right thing to do would be to interpolate |
That counts as "after the returned tree is rendered" - you'd get a similar result if you simply did
I was talking about the class of problems solved by having
No, just the ability to return the current vnode (the one we're updating). Anything else would still continue to throw an error, including returning the updated one. |
Yes of course, but my point remains - you can do away with The idea occurred to me when thinking about how spurious React's new hooks are - essentially strange exotic first class entities based on timing conditions. As we've determined WRT the issue of DOM paint ticks in I agree with your assessment that
Show me what the desired user-land code would look like? |
It's still useful to at least schedule in the current tick for after rendering. And if we don't offer that hook, But also, I'd rather not rely on users having to do some serious hack just to schedule some work. We could solve this via an This would mean we could change the component instance to just a plain
When attempting the first render, we'd differentiate pure instances from stateful components when invoking the component the first time. If it returns a function, we treat that as the instance and invoke it every time. If it returns a tree, we treat the component itself as the instance and just use the initial tree. Here's what that'd end up looking like:// v2
function FriendWithStatusCounter() {
let count = 0
let isOnline, prevId
function onChange(status) { isOnline = status.isOnline; m.redraw() }
return {
oncreate({attrs}) {
document.title = `You clicked ${count} times`
ChatAPI.subscribeToFriendStatus(attrs.friend.id, onChange)
prevId = attrs.friend.id
},
onupdate({attrs}) {
document.title = `You clicked ${count} times`
if (prevId !== attrs.friend.id) {
ChatAPI.unsubscribeFromFriendStatus(prevId, onChange)
ChatAPI.subscribeToFriendStatus(attrs.friend.id, onChange)
prevId = attrs.friend.id
}
},
view: () =>
isOnline === null ? 'Loading...'
: isOnline ? 'Online'
: 'Offline',
}
}
// This proposal
function FriendWithStatusCounter() {
let count = 0
let isOnline
function onChange(status) { isOnline = status.isOnline; m.redraw() }
return ({attrs} = {}, {attrs: prev} = {}) => {
document.title = `You clicked ${count} times`
if ((attrs && attrs.friend.id) !== (prev && prev.friend.id)) {
if (prev) ChatAPI.unsubscribeFromFriendStatus(prev.friend.id, onChange)
if (attrs) ChatAPI.subscribeToFriendStatus(attrs.friend.id, onChange)
}
return isOnline === null ? 'Loading...'
: isOnline ? 'Online'
: 'Offline'
}
} And for your example: return (vnode, prev, update) => {
if (!prev) { // first pass
oninit()
update(oncreate, vnode)
return initialView
} else if (!vnode) { // last pass
const result = onbeforeremove(prev)
if (typeof result.then !== "function") update(onremove, vnode)
else result.then(() => update(onremove), () => update(onremove, vnode))
} else if (!onbeforeupdate(vnode, prev)) ( { // subsequent passes
return prev
} else {
update(onupdate, vnode)
return furtherView
}
}
I can sympathize, and conditionally async is usually a PITA. Just in this case, it's much easier to use if we do allow conditionally async behavior.
Something like this: // v2
function CounterButton() {
let count = 0
let updating = false
return {
onbeforeupdate(vnode, old) {
if (updating) { updating = false; return true }
return vnode.attrs.color !== old.attrs.color
},
view(vnode) {
return m("button", {
style: {color: vnode.attrs.color},
onclick: () => { updating = true; count++ }
}, "Count: ", count)
},
}
}
// The `view`/`onupdate`/`onbeforeremove` proposal
function CounterButton() {
let count = 0
let updating = true
return {view(vnode, old) {
if (!vnode) return undefined
if (updating) updating = false
else if (vnode.attrs.color === old.attrs.color) return old
return m("button", {
style: {color: vnode.attrs.color},
onclick: () => { updating = true; count++ }
}, "Count: ", count)
}}
}
// The `render` function proposal
function CounterButton() {
let count = 0
let updating = true
return (vnode, old) => {
if (!vnode) return undefined
if (updating) updating = false
else if (vnode.attrs.color === old.attrs.color) return old
return m("button", {
style: {color: vnode.attrs.color},
onclick: () => { updating = true; count++ }
}, "Count: ", count)
}
} We could take the component-as-a-function one step further and make it a proper reducer, making components and Here's what that'd end up looking like:
If you'd like to see something a little more stateful to show the reducer side, here's what the function CounterButton(vnode, old, update, state = {count: 0, updating: true}) {
if (!vnode) return undefined
if (state.updating) state.updating = false
else if (vnode.attrs.color === old.attrs.color) return old
return {next: state, view: [
m("button", {
style: {color: vnode.attrs.color},
onclick: () => { state.updating = true; state.count++ }
}, "Count: ", state.count)
]}
} |
I see what you're saying about async effects invoked in views not matching up with the current implementation of update - they would happen in a new tick, while currently onupdate occurs synchronously immediately after views have persisted. Does it particularly matter, in practice? It's neat to have onbeforeremove mirror initial iteration |
It's a concern for us because we need to block actual DOM removal (#1881 (comment)), but I agree it's not generally a concern elsewhere. I do also agree we don't really need scheduling in core, but it'd be nice to have something to direct users towards, even if it's not within I see two main options here:
The first is actually incredibly simple to implement.var scheduled = []
var promise = Promise.resolve()
function report(e) { promise.then(function () { throw e }) }
function execute() {
for (var callback; (callback = scheduled.shift()) != null;) {
try { callback() } catch (e) { report(e) }
}
}
m.update = function (callback) {
if (typeof callback !== "function") {
throw new TypeError("`callback` must be a function!")
}
if (scheduled.length === 0) promise.then(execute)
scheduled.push(func)
} This isn't just I seriously wish there was a direct language-level means of scheduling microtasks, though...
What about going with the reducer, but changing it to |
BTW, I'm starting to like the idea of |
Just updated and rewrote the proposal entirely. |
I wish you could just post that as new stuff, the discourse and mutual reasoning becomes impossible to trace when posts are edited to such significant degrees |
@barneycarroll I understand. BTW, I did make it a point to do a couple things:
The reason why I redid the proposal and edited it instead of filing a new issue is because although it was substantially different, the previous proposal did almost nothing to solve the real issues with lifecycle hooks. It also didn't really match the spirit of its changes - it was simplifying it for us, but not for users. Saving 20-30 bytes by pulling a popular feature isn't doing anyone any real favors, especially since that requires migration and everything. It was only after a bit of reflection after reading some of the responses here that I realized I was missing a major opportunity to actually fix things by really simplifying components (the title of the issue), not just for us but also for users. And of course, this realization meant I had to basically trash my proposal and start over. This issue was still the most appropriate place to discuss it, but the original proposal itself became mostly irrelevant. I will make a counterpoint to not editing the main proposal: it's much easier to follow and understand what the proposal is if it's not spread across several comments in a conversation. It's also why Gitter is not a good place to have more complicated design discussions, and it's why many larger design-related proposals elsewhere end up with dedicated repos and everything. I've personally been considering opening up a new orphan branch on my personal fork to expand on #2278/#2284 and all its sub-issues, simply because of how broad and inter-connected it's become. (Once you find yourself needing intra-page hyperlinks, that's a good time to look into it.) Now I really shouldn't be editing subsequent comments, even if they're just bug fixes. |
Initial proposal
Based on #2293 and recent Gitter discussion, I think we could get away with simplifying our component model some in v3. **Edit:** My proposal has since expanded and got a little broader - https://github.com//issues/2295#issuecomment-439846375.vnode.state
would just be set tovnode.tag
.vnode.oninit
would disappear in favor of closure component constructors.vnode.state
tovnode._hooks
so it's clearer it's meant to be internal.The reason for this:
oninit
with a constructor step, and with the only reason they exist (stateful object components) removed, it's kinda useless.Here's most of the implementation changes.
This simplifies this function to this:
BTW, this route is way more flexible than React's function components (pre-hooks):
Updates
e.redraw
update
identitym.bind
m.changed
helperThis is part 2 of a broader proposal: #2278.
Let's simplify our component model for v3.
So far, every single one of our component types suck in some way:
vnode.state = Object.create(null)
, causing user confusion on a frequent basis when they mistakenly donew Comp(...)
becausethis === Object.create(new Comp(...))
, notthis === new Comp(...)
. They also get really verbose because in order to avoidthis
issues, you're stuck withvnode.state.foo
and similar, and destructuring isn't a real alternative when mutations get involved.this
issues. React users extensively use bound class methods (method = (...args) => this.doSomething(...args)
) to work around this, and it's no different with us.vnode
argument.Also, every one of our lifecycle methods have issues:
oninit
is 100% duplicated by class constructors and closure factories.onbeforeupdate
could literally be solved by a "don't redraw" sentinel + passing both parameters to the view. This also has other benefits: Extend (present, previous) signature to all component methods #2098.onbeforeremove
andonremove
are basically one and the same. You could get equivalent behavior by just calling the parentonremove
, awaiting it, then calling the childonremove
s depth-first without awaiting them.oncreate
andonupdate
are unnecessary boilerplate - we could just as easily switch to am.update
that just schedules callbacks on the next tick.1, and you'd call this in the same place you'd render.And finally, our component model is reeking of broken, leaky abstractions.
m.request
to DOM event handlers), yet we still frequentlym.redraw
.onremove
after it's loaded.vnode.state
being made no longer assignable in v2 for performance reasons.onupdate
as I do inonbeforeupdate
.Proposal
So here's my proposal: let's model components after reducers. Components would be single simple functions with four simple parameters:
undefined
if it's the first render.Components would return either a vnode tree or an object with three properties:
view
(required): This is the vnode tree you plan to render.next
(required): This is thevnode.state
to use in the next component call.onremove
(optional): This is called when a component is removed from the DOM.The hook
onremove
would be called on the parent node first, awaited from there, and then on resolution, recursively called on child elements depth-first without awaiting.The
vnode.state
property would become the current state, andold.state
would represent the previous state.vnode.update
is how you update the current state, and it replaces the functionality ofevent.redraw
andm.request
'sbackground: false
. When called, it sets the next state synchronously and invokesm.redraw()
.update
itself is a constant reference stored for the lifetime of the component instance, for performance and ease of use - most uses of it need that behavior.This would, of course, be copied over between
vnode
andold
.Here's how that'd look in practice:
For comparison, here's what you'd write today:
Attributes
If I'm going to ditch lifecycle attributes, I'll need to come up with similar for elements. This will be a single magic attribute
onupdate
like the above, with the samevnode
andold
, but returned vnode trees and returned state are ignored - you can only diff attributes. In addition, the thirdupdate
argument does not exist. There's reasons for ignoring each:You can still return
old
or{view: old, ...}
to signify "don't update this vnode". That's the only reasonview
is ever read.Mapping
The hooks would be fused into this:2
oninit
would become just the first component call. The component would be rendered astag(vnode, undefined, undefined, update)
, and the first render can be detected by the previous vnode beingundefined
.oncreate
would become invokingm.update
in the first component call.onbeforeupdate
would be conditionally returningold
from the component.view
would be returning the vnode tree from either the top level or via theview
property.onupdate
would be invokingm.update
on subsequent component calls.onbeforeremove
would be optionally returning a promise fromonremove
.onremove
would be returning a non-thenable fromonremove
.Helpers
There are two new helpers added:
m.update
andm.changed
.m.update
is literally just sugar for scheduling a callback to run on the next tick, but this is practically required for DOM manipulation.m.bind
dramatically simplifies things like subscription management, by implicitly cleaning things up for you as necessary.m.changed
is a simple helper function you call asconst changed = m.changed(state, key, prevKey)
, and is a cue for you to (re-)initializestate
. For convenience,m.changed(state, vnode, old, "attr")
is sugar form.changed(state, vnode.attrs[attr], old && old.attrs[attr])
, since the common use here deals with attributes.false
, you should continue usingstate
as necessary.true
, it invokesstate.close()
if such a method exists and signals to you that you should reinitialize it.state == null
, this returnstrue
.Why this?
I know this is very different from the Mithril you knew previously, but I have several reasons for this:
Hooks vs reducer
When you start looking into how the hooks work conceptually, it stops being as obvious why they're separate things.
oncreate
/onupdate
are always scheduled after every call toview
, and the code to explicitly schedule them is sometimes simpler than defining the hooks manually. In this case, you might as well put them in theview
directly - you'll save Mithril quite a bit of work.oninit
>view
> scheduleoncreate
. If you couple the current state to the current view, you can merge this entire path.onbeforeupdate
>view
> scheduleonupdate
. If you provide the previous attributes, allow returning a "don't update the view" sentinel, and couple the current state to the current view, you can merge this entire path.And so because they are so similar, I found it was much simpler to just reduce it to a single "reducer" function. For the first render, it's pretty easy to check for a missing previous vnode (
old == null
).If you'd like to see how this helps simplify components:
This non-trivial, somewhat real-world example results in about 25% less code. (It's a component wrapper for Bootstrap v4's modals.)
State
I chose to make state in this iteration with a heavy preference for immutability because of two big reasons:
I also coupled it to the view so it's easier to diff and update your state based on new attributes without having to mutate anything.
It's not completely anti-mutation, and it's not nearly as magical as React's
setState
- it's more like theirreplaceState
. You can still do mutable things in it and it will work - you could take a look at theMedia
component definition here3. It just makes it clear that in smaller scenarios, there may be easier ways to do it, and it encourages you to do things immutably with smaller state. For large, stateful components, it's still often easier to just mutatestate
directly and dovnode.update(state)
- you get the benefits of partial updates without the cost of constantly recreating state.No auto-redraw anymore?
Generally not. But because
update
both replaces the state and auto-redraws,Motivation
My goal here is to simplify Mithril's component model and help bring it back to that simplicity it once had. Mithril's components used to only consist of two properties in v0.2: a
controller
constructor and aview(ctrl, ...params): vnode
method. You'd use this viam(Component, ...params)
, and it would be fairly consistent and easy to use.4It also had far fewer lifecycle hooks - it only had two:
attrs.config(elem, isInit, ctx, vnode)
for most everything that requires DOM integration andctrl.onunload(e)
/ctx.onunload(e)
for what we haveonremove
for. If you wanted to do transitions, you'd dom.startComputation()
before you trigger it andm.endComputation()
after it finishes, and this would even work inonunload
. If you wanted to block subtree rendering, you'd pass{subtree: "retain"}
as a child.5I miss that simplicity in easy-to-understand component code, and I want it back. I want all the niceness of v0.2's components without the disappointment.
Notes
m.update
would literally be this. It's pretty easy, and it's incredibly easy to teach. It's also simpler to implement than evenm.prop
orm.withAttr
.For a more concrete explanation of how hooks correspond to this, here's how I could wrap legacy v1/v2 components for migration:
Here's the source code for the
Media
component, used in an example in Make the router use components, but only accept view functions #2281. It's basically a stripped-down port ofreact-media
.Well...not always easy. Mithril v0.2 didn't normalize children to a single array, so proxying children got rather inconvenient at times. What this means is that if you used
m(Comp, {...opts}, "foo", "bar")
, the component'sview
would literally be called asComp.view(ctrl, {...opts}, "foo", "bar")
, so you'd often need to normalize children yourself. It also didn't help that this was all pre-ES6, or it would've been a little more tolerable.Most of the internal complexity and bugs in v0.2 were caused indirectly by one of two things: the spaghetti mess of a diff algorithm and
m.redraw.strategy()
causing hard-to-debug internal state issues thanks to constantly ruined assumptions. Oh, and insult to injury:e.preventDefault()
inctrl.onunload
orctx.onunload
means you can't even assume unmounting the tree clears it or that route changes result in a fresh new tree anywhere. It's all these kinds of papercuts caused by internal bugs in a seriously messy code base that led Leo to rewrite and recast the entire API for v1.The text was updated successfully, but these errors were encountered: