Choo: Upgrade guide

Created on 6 Jul 2016  路  8Comments  路  Source: choojs/choo

Just upgrading my 2.0 app to 3.0 and wanted to document what's involved for anyone else upgrading.

  • Views previously used choo.view to construct HTML. Now you should use const html = require('choo/html')
  • Views previously received (params, state, send) as params. They now receive (state, prev, send)
  • Any references to params inside the views should now point to state.params
  • Any references to the current location should now point to state.location....?
  • Effects and subscriptions now have a new parameter, done, which should either be explicitly called inside your effect or passed to your final send() call
  • Effects and subscriptions which call send() multiple times should nest them inside the done callback, ie:
send('foo', {...}, () => {
  send('bar', {...}, done)
})
  • Reducers and effects formerly received a first parameter called action. The convention is now to call it data (optional of course)
  • The data param passed to reducers and effects does not need to be an object, so you can do things like send('setTitle', 'New Title'), which can improve readability

A pretty easy upgrade. The done business is probably the only confusing part. Of course, this is just what I encountered in my app. I believe @toddself also upgraded an app recently and may have had to do other things.

Most helpful comment

Gehe - yeah I think async flow is amongst the hardest compsci things out there; coupled with a half-assed explanation in the docs there's def no need to feel silly for not getting it straight away - always happy to help! :sparkles:

All 8 comments

I also updated my firebase example app to 3.0 and got pretty confused by the done business. I just ended up adding the done to all effect send() calls, probably not the correct usage? Would love it if someone could clear this up for me. 鉂わ笍

Hmm yeah so done() is to signal if an effect is done executing or has encountered an error. An example xhr call:

const http = require('choo/http')

module.exports = {
  effects: { performRequest: request }
}

const uri = '/good'
function request (data, state, send, done) {
  http(uri, { json: true }, function (err, res, body) {
    if (err) return done(new Error('HTTP error'))
    if (res.statusCode !== 200) {
      const message = (body && body.message)
        ? body.message
        : 'unknown server error'
      return done(new Error(message))
    }
    if (!body) return done(new Error('fatal: no body received'))
    send('api:set', body.message || body.title, done)
  })
}

You'll notice here that if at any point an error occurs we return done(err). Only if everything succeeds do we call send and pass done() as the final argument so that errors will bubble up the stack. Does that make sense?

I can't fully wrap my brain around the intended usage. Taking @mw222rs' attemptLogin effect:

attemptLogin: (data, state, send, done) => {
  send('auth:attemptingLogin', done)
  const provider = new firebase.auth.GithubAuthProvider()
  auth.signInWithPopup(provider).catch(error => {
    send('feedback:displayError', { error }, done)
    send('auth:logout', done)
  })
}

What would be the correct usage of done here? The first send call and/or the last send in the error callback?

Unrelated to done we stumbled upon another change; when bootstrapping an app we previously did this:

app.router((route) => [route('/', Submission(app))])

...where Submission is a component that also made an app.model call. This no longer worked in v3, and had to be changed to

const root = Submission(app)
app.router((route) => [route('/', root)])

I'm guessing somehow the new architecture required the app.model calls to have been made before the routing.

@krawaller so send() calls are (and always have been) asynchronous, which means that if you place them one after another they might race - and in this exact case done() will be called multiple times.

What changed in v3 is that send() calls now signal when they're done executing, so call order can be guaranteed - they no longer race. So to rewrite the example to work in order:

attemptLogin: (data, state, send, done) => {
  send('auth:attemptingLogin', (err, res) => {
    if (err) return done(err)
    const provider = new firebase.auth.GithubAuthProvider()
    auth.signInWithPopup(provider).catch(error => {
      send('feedback:displayError', { error }, (err, res) => {
        if (err) return done(err)
        send('auth:logout', done)
      })
    })
  })
}

Though probably best would be to have a generic onError() hook, and let that call 'feedback:displayError' if an error occurs. That way the error handling logic becomes generic in the application, and new functionality only has to care about bubbling errors back up - not the exact specifics of handling errors. Using run-series this would look like:

attemptLogin: (data, state, send, done) => {
  const fns = [
    (cb) => send('auth:attemptingLogin', cb),
    (cb) => {
      const provider = new firebase.auth.GithubAuthProvider()
      auth.signInWithPopup(provider).then(cb).catch(cb)
    }
  ]
  series(fns, done)
}

Instead of run-series, promisifying each send() call should also work; or using pull-stream too. Does this make sense?

Also: could you create a separate issue for the model / router order thing? I want to verify that, but reckon it's best to keep these discussions separate.

@yoshuawuyts That makes a ton of sense! Feel silly for not grokking it, seems so obvious now. Thank you for your help and patience!

Creating issue for the model-route thing right away!

Gehe - yeah I think async flow is amongst the hardest compsci things out there; coupled with a half-assed explanation in the docs there's def no need to feel silly for not getting it straight away - always happy to help! :sparkles:

It's been almost a month since choo@3 - I reckon we can close this issue for now :sparkles: - going forward I think it'd be best to add an "upgrade" doc to the choo handbook :rocket:

Was this page helpful?
0 / 5 - 0 ratings