Discussion: choo/component
See original GitHub issueupdate (03/11/17)
The initial design in this post isn’t great; here’s a revised version with support for async loading.
components
When building out applications, components are a great way to slice UI and its corresponding logic into crisp slices.
We’ve created nanocomponent
as a solution to allow for encapsulation with DOM
diffing. Unforunately it seems to be a tad verbose, which means people have
gone around and built wrappers to make things a little simpler.
These wrappers seem to be mostly focused on two things:
- Manage component instances.
- Remove boilerplate involved with create a component.
Challenges
- Because of the modular nature of components, we can’t do the centralized tangly thing that other frameworks can do. Unfortunately 😦.
- We also took it upon ourselves to use “The Web Platform”. This means we’re limited by odd behavior from the DOM. Not great, but it’s a tradeoff we’ve taken.
- Most of what we do is about explicit behavior — explicit re-renders, explicit updates, explicit mutations. Being overly explicit can result in boilerplate, and repetition — the art is to find balance.
Example
A basic component looks kinda like this:
var component = require('choo/component')
var html = require('choo/html')
module.exports = class Button extends component {
createElement () {
return html`
<button>
Click me
</button>
`
}
update () {
return false
}
}
But that’s not super debuggable. Ideally we would allow devtools to inspect all of the state, events, and timings.
var component = require('choo/component')
var html = require('choo/html')
module.exports = class Button extends component {
constructor (name, state, emit) {
this.state = state
this.emit = emit
super(name)
}
createElement () {
return html`
<button onclick=${this._onclick}>
Click me
</button>
`
}
update () {
return false
}
_onclick (e) {
this.emit('increment', 1)
}
}
Ok cool, now we can emit events on the global emitter. When the component is
constructed it just needs to be passed the elements. This can be done with
something like shared-component
. It requires a bit of boilerplate — the exact
implementation is not the point.
Instead I was thinking it might be neat if we could do component registration in a central spot — as a new method on an instance of Choo.
var choo = require('choo')
var app = choo()
app.component('button', require('./components/button'))
app.route('/', require('./views/main'))
app.mount('body')
Yay, so now we have component registration going on. Folks can proceed to slice
up state
in whatever way they want. To render components, I was thinking we’d
add another method to views
so components become accessible.
var html = require('choo/html')
module.exports = function view (state, emit, components) {
return html`
<body>
${components.button.render()}
</body>
`
}
components
here is an object, similar in behavior to shared-component
. I
considered stealing some of component-box
’s behavior, and abstract away
the .render()
method — but when a component requires another component, it
wouldn’t have any of those abstractions, and we’d introduce two different APIs
to do the same thing. I don’t think that might be a good idea. Or maybe we
should do it? Argh; this is a tricky decision either way (e.g. consistency +
learning curve vs ease of use, half the time).
Oh, worth noting that because components can now be registered with Choo, we should probably extend Nanocomponent to not require the whole boilerplate:
var component = require('choo/component')
var html = require('choo/html')
module.exports = class Button extends component {
createElement () {
return html`
<button onclick=${this._onclick}>
Click me
</button>
`
}
update () {
return false
}
_onclick (e) {
this.emit('increment', 1)
}
}
Ok, that was cool. Exciting!
Yeah, I know — I’m pretty stoked about this too. Now it is worth pointing out that there’s things we’re not doing. Probably the main thing we’re not doing is managing lists of components. Most of this exists to smooth the gap between application-level components, and the views itself. Our goal has always been to allow people to have a good time building big app — whether it’s getting started, debugging or tuning performance.
Now there’s some stuff we’ll have to do:
- We’re showing the
class
keyword everywhere. I think it’s useful when building applications because it’s way less typing than alternatives. Bankai would need to support it. - We’d need to teach people how to use (vector) clocks to keep track of updates. I don’t like shallow compares, and just so much boo — let’s have some proper docs on the matter.
- We need to think about our file size marketing. You can totes write a whole app without any components, and like we should promote it — it’s heaps easy. But with components, certain patterns become kinda nice too. Is it part of choo core? Wellll, kinda — not core core — but core still.
What doesn’t this do?
Welllllll, we’re not doing the thing where you can keep lots of instances of the same component around. I think dedicated modules for that make sense — Bret’s component array thing is probably a good start, and work for more specialized cases from there. For example having an infinite scroller would be neat.
When will any of this land?
Errrrr, hold on there — it’s just a proposal for now. I’m keen to hear what y’all think! Does this work? Where doesn’t this work? Do you have any ideas of how we should do a better job at this? Please share your thoughts! 😄
Summary
Wellllllp, sorry for being super incoherent in this post; I hope it sorta makes sense. The Tl;Dr is:
add a new method for Choo called .component
, which looks like this:
index.js
var choo = require('choo')
var app = choo()
app.component('button', require('./components/button'))
app.route('/', require('./views/main'))
app.mount('body')
components/button.js
var component = require('choo/component')
var html = require('choo/html')
module.exports = class Button extends component {
createElement () {
return html`
<button onclick=${this._onclick}>
Click me
</button>
`
}
update () {
return false
}
_onclick (e) {
this.emit('increment', 1)
}
}
views/main.js
var html = require('choo/html')
module.exports = function view (state, emit, components) {
return html`
<body>
${components.button.render()}
</body>
`
}
Issue Analytics
- State:
- Created 6 years ago
- Comments:76 (64 by maintainers)
Top GitHub Comments
Updated choo-component-preview to v2.0.0. It now makes use of an LRU cache to evict entries, and has a more explicit API (no more
static id
method required), addressing some of the points @bcomnes raised.If hope this strikes a good middleground between ease of use and having an explicit API.
People on IRC seemed to be fairly into this; if people are good with it here too I might take a stab at updating #606 so we can think of landing this! 🎉
I’m also thinking of ways to address changes of this magnitude in the future. Discussion in this thread ended up with a long tail; perhaps having an explicit RFC proposal might help us focus on defining problems & solutions a bit better. Input on how to improve communication would be very welcome!
I’ve been thinking about that as well. In my proposal I used the unload hook but don’t know if that has any implications on performance? Also, it’d be nice if the user could override the GC for components that are very expensive to initialize and would want to stick around after being unmounted.
I couldn’t help myself and made an implementation of the whole component registration thing, again using
static
methods. The static methodregister
with the same signature as application stores (arguments being state and emitter) on the nanocomponent constructor would add on the emit handler to its prototype when called, much like how @jongacnik suggested. Also, for asynchronous registration, we’d add theregister
event that calls the register method of the given constructor.Putting it all together, async components and all: https://choo-component-register.glitch.me Source: https://glitch.com/edit/#!/choo-component-register?path=index.js:1:0
Pros
app.use
, no need for a new interface.register
method and do all kinds of cool stuff now that the state and emitter are exposed.Cons