question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Discussion: choo/component

See original GitHub issue

update (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:

  1. Manage component instances.
  2. 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>
  `
}

cc/ @bcomnes @jongacnik @tornqvist

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Comments:76 (64 by maintainers)

github_iconTop GitHub Comments

10reactions
yoshuawuytscommented, Mar 7, 2018

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.

var Article = require('./components/article')
var Header = require('./components/header')
var Footer = require('./components/footer')

module.exports = function (state, emit) {
  return html`
    <body>
      ${state.cache(Header, 'header').render()}
      ${state.articles.map(article => {
        return state.cache(Article, article.id).render(article)
      })}
      ${state.cache(Footer, 'footer').render()}
    </body>
  `
}

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!

3reactions
tornqvistcommented, Nov 7, 2017

The question becomes, how do we GC? Add unload hooks? Wait till idle in the browser and look?

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 method register 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 the register 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

  • The public API of choo and nanocomponent remain unchanged (or rather, no breaking changes).
  • Asynchronous components would be registered using the event emitter, giving the whole app a chance to act on, and access the new component.
  • Registering 3rd party components with the application would use app.use, no need for a new interface.
app.use(require('some-3rd-party-component').register)
  • One can define a custom static register method and do all kinds of cool stuff now that the state and emitter are exposed.
class State extends Component {
  static register (state, emitter) {
    super.register(state, emitter)
    State.prototype.state = state
  }
  createElement () {
    return html`<pre>${JSON.stringify(this.state, null, 2)}</pre>`
  }
}

Cons

  • nanocomponent would be baked into choo. This is really just a design decision of mine so as users wouldn’t have to register it manually. I don’t know if it’d bring us over 4kb but if that’s a pain point we could just encourage users to register choo/component themselves.
app.use(require('choo/component').register)
Read more comments on GitHub >

github_iconTop Results From Across the Web

Stupidly smart components in Choo - krawaller
Take a traditional React app (or any app, really). Most components are (hopefully) dumb, meaning they have no state of their own -...
Read more >
choo-component-preview - npm Package Health Analysis - Snyk
Learn more about choo-component-preview: package health score, popularity, security, maintenance, ... See discussion for more information.
Read more >
Choo Choo (week 44, '17). Heyyy, hello. How are ... - Medium
It's no secret that we want to add a component system to Choo. There's a discussion issue on choojs/choo#593. The current state of...
Read more >
Secrets of a Luxury Agent with Christophe Choo (Part 2 of 2)
Secrets of a Luxury Agent with Christophe Choo ( Part 2 of 2)Beverly Hills luxury agent and specialist Christophe Choo is so full...
Read more >
I bought the game and it wont open - Steam Community
I just bought Choo Choo Charles and when I got to open it. It says "The following component(s) are required to open this...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found