I want to gather a bit of sentiment for what people might be missing / what you feel might improve. The same as in v6, it'd be nice if all that was needed to update the major was npm install --save choo@7, and that's it.
So far my wish list is:
PerformanceObserver like in choo-log.xtend with Object.assign() - https://polyfill.io provides a fallback for browsers that don't support it, so for most people this will be a strict improvement.choo/component, using nanocomponent - Ideally it'd be nice if we could formalize tracing / an event system with this too. That way we keep blazing ahead in terms of debugging.window.initialState) to provide out of the box "rehydration" of state that was passed on the server.It's still early, but I'd be interested in hearing input from people on what you find tricky, which things you love, and which things you struggle with! Thanks! :sparkles:
I would love if we could figure out a way to get code-splitting to allow for code-split bundles to gain access to app.use without using the window to store your app instance to it's accessible.
var app = typeof window.app === 'undefined' ? choo() : window.app
It would be nice to use barracks seamlessly again...
Also, bringing nanocomponent closer will be awesome to build some stateful components :)
Point no. 1 - routes rendered async, you might want to check out universal-router, i think they got it quite right, I also implemented their router in many side projects of mine.
Excerpt:
const router = new UniversalRouter({
path: '/hello/:username',
async action({ params }) {
const resp = await fetch(`/api/users/${params.username}`);
const user = await resp.json();
if (user) return `Welcome, ${user.displayName}!`;
},
});
Of course, this is more verbose with additional property names since choo doesn't use these - its, however, trivial to omit :-)
Some clever way to do page transitions/animations. There has been some discussion before in #487 I'm particulary interested in page transitions, because element component specific animations should be resolved particullary in views or nanocomponents IMO
Extending router by a before hook would just do, wouldn't it? Its also non-breaking:
app.route(route, component, beforeHook)
Here's a radical thought regarding async routes: if they were generator functions one could progressively render a view as data/resources become available.
Also, I do miss a standard way of handling errors in choo. Wrapping the route handlers with a try/catch and emitting a standard error event upon a route crashing or a yielded Promise rejecting would be super!
var html = require('choo/html')
var choo = require('choo')
var app = choo()
app.use(api)
app.route('/', mainView)
app.mount('body')
function * mainView(state, emit) {
if (state.error) {
return html`
<body>
<h1>Oops!</h1>
<p>This happened: ${state.error}</p>
<button onclick=${onclick}>Try again</button>
</body>
`
function onclick() {
emit('fetch')
}
}
if (!state.data) {
yield html`
<body>
<h1>Loading</h1>
</body>
`
yield emit('fetch')
}
return html`
<body>
<h1>${state.data}</h1>
</body>
`
}
function api (state, emitter) {
state.data = null
emitter.on(state.events.ERROR, function (err) {
state.error = err.message
emitter.emit(state.events.RENDER) // Override current render cycle
})
emitter.on('fetch', async function () {
var response = await fetch('/api')
state.data = await response.text()
state.error = null
emitter.emit(state.events.RENDER) // Ignored during async render cycle
})
}
Can a build with assert's removed be published alongside the standard build?
I had some difficulty 'unassertifying' via webpack (got it working in the end) and the assert library is quite large so it'd be nice to have a simpler way to reduce bundle size.
@chrisdwheatley nice one! - yeah was thinking we move to https://github.com/emilbayes/nanoassert using the package.json browser field. We've done that for a few modules already, and it seems to work real well :grin:
I think no1 priority should be the apparent performance issue as of now, thats a rather big issue imo
@graforlock Any particular examples?
Not many real world ones (not in a simple SPA website) but creating, updating and appending thousands of divs obviously keep recreates the full DOM paint and maybe needs some good caching/memoizing mechanism (if possible).
I started doing this for async routes, it's nothing fancy or revolutionary but I'm putting it here in case it might come in handy in figuring out a framework-level solution.
I needed it because I'm doing server-side rendering and want to make sure I fetch the data before
I return the initial render.
// client side
app.use(function asyncInitialState () {
return function (state, emitter, app) {
const routeFn = app.route.bind(app)
const onCallback = initialState => Object.assign(state, initialState)
app._getInitialState = {}
app.route = function (routeName, fn, getInitialState) {
routeFn(routeName, fn)
app._getInitialState[routeName] = getInitialState
}
emitter.on(state.events.NAVIGATE, () => {
const getInitialState = app._getInitialState[state.route]
if (!getInitialState) return
getInitialState(onCallback)
})
}
})
app.route('/blog', layout(blogPage), async () => {
const initialState = {
blog: {
fetching: false,
posts: await fetch(endpoint).then(res => res.json())
}
}
return initialState
})
// server side
async function routeHandler (req, res, next) {
const pathname = url.parse(req.url).pathname
if (regex.test(pathname)) return next()
var initialState = {}
var getInitialState = client._getInitialState[pathname]
if (getInitialState) initialState = await getInitialState()
const html = client.toString(pathname, initialState)
pump(req, res)
res.end(html)
}
I think we it's possible to rewrite the render handling in such a way that it can support both sync and async routes without changing the syntax. This is loosely based on the way Gulp deals with the fact that tasks can both be sync and async. (I've removed some nanotiming calls for clarity.)
this.emitter.prependListener(self._events.RENDER, nanoraf(function () {
self.state.href = self._createLocation()
var res = self.router(self.state.href)
if (res instanceof HTMLElement) {
var newTree = res
nanomorph(self._tree, newTree)
} else if (res && res.then typeof === 'function') {
res.then(function (newTree) {
assert(newTree instanceof HTMLElement, '...')
nanomorph(self._tree, newTree)
})
}
}))
This way the current sync routes operate in the same way as they do now, plus you can also pass in a route handler which returns a promise (or just an async function, since they're based on promises under the hood).
It might be possible to support callbacks too, as a third argument for route handlers, i.e. (state, emitter, done). If we added the required support to nanorouter, that would change the above to:
this.emitter.prependListener(self._events.RENDER, nanoraf(function () {
self.state.href = self._createLocation()
var res = self.router(self.state.href, function (err, newTree) {
if (err) return //not exactly sure how to handle this yet, feedback welcome :p
assert(newTree instanceof HTMLElement, '...')
nanomorph(self._tree, newTree)
})
if (res instanceof HTMLElement) {
var newTree = res
nanomorph(self._tree, newTree)
} else if (res && res.then typeof === 'function') {
res.then(function (newTree) {
assert(newTree instanceof HTMLElement, '...')
nanomorph(self._tree, newTree)
})
} else {
//do nothing, should be handled by the callback above
//maybe some kind of assertion could be here added to be sure
}
}))
I've experimented a bit with async routes on my own fork, and it seems the biggest challenge would be to keep supporting the app.toString() functionality, which should probably always stay synchronous.
Maybe an optional async resolve mechanism would be more appropriate. A possible API could be:
app.route('/', mainView, {resolve: fetchThings})
function mainView (state, emit) {
return html`<body>
<ul>${state.things.map(listItem)}</ul>
</body>`
}
function fetchThings (state, done) {
api.get('/things', function (err, res) {
if (err) {
return done(err)
}
state.things = res.data
done()
}
}
I'm closing this since v7.0.0 has been published and the discussion on async render has been picked up elsewhere: https://github.com/choojs/choo/pull/653 https://github.com/choojs/choo/pull/646 https://github.com/choojs/choo/issues/645
Most helpful comment
@chrisdwheatley nice one! - yeah was thinking we move to https://github.com/emilbayes/nanoassert using the
package.jsonbrowserfield. We've done that for a few modules already, and it seems to work real well :grin: