Choo: Hot module replacement?

Created on 26 Oct 2016  ·  13Comments  ·  Source: choojs/choo

So I'm going to start with the the premise that _Hot Module Replacement™_ should be as easy and pluggable as https://github.com/substack/react-starter-hmr, with minimal change to the client application.

screen shot 2016-10-26 at 11 43 51

This is a rough-guide on what is required to get it set up:

  1. Install the dependencies: npm i --save-dev browserify-hmr ud
  2. Add the plugin to the browserify/watchify bundler watchify client.js -p browserify-hmr ...
  3. Add the ud.defn(...) annotation to the part of the application to show which bits of the program are suitable for hot-reload (per screenshot above)
  4. Watch view updates appear insta-matically in the page _without_ losing state

My trouble is that with step 4. I couldn't find a way with choo to force a re-render of the page's current state like in the React example. I got close in two different ways - forcing a reboot of the entire app (so reverting to initial state) or views updating the _next_ time they were rendered (not insta-matically).

If anyone knows the secret to getting this set up, please tell! Otherwise if a change is required in choo to facilitate this, let's figure it out? I think it's going to be something like exposing a way to call render/re-render, unless I'm missing something!

Most helpful comment

I've published v1 of both https://github.com/bengourley/choo-resume and https://github.com/bengourley/hot-rld.

@yoshuawuyts hot-rld now supports css too.

All 13 comments

Excited for this. This is probably a hacky idea, but the page should be re-rendered any time a reducer is fired, regardless of whether the state was updated. So you could inject a reducer that simply returns the current state, thereby not changing it but re-rendering the page anyway. But I'm sure there's a more elegant way if we change something in choo.

Something magic (read: something I haven't figured out yet 😁 ) causes the React example to re-render, even though it seems to just be updating the class/prototype of the <App /> component.

@timwis I'm going to give your suggestion of a noop-y reducer a try and see if that sorts us out!

Ok, brace yo'selves… here's how HMR is achievable in choo right now:

  • npm install --save-dev browserify-hmr ud
  • add -p browserify-hmr to your browserify options
  • every "route" (i.e every view that you provide to the router) must be wrapped in the ud.defn() call like so:
module.exports = ud.defn(module, (state, prev, send) => {
  return html`
    <div>Blah</div>
  `
})

(here's the nastiest bit!)

  • define a noop reducer and a subscription that repeatedly calls it:
  ...
  reducers: {
    noop: (action, state) => ({})
  },
  subscriptions: [
    (send, done) => {
      setInterval(() => send('noop', done), 1000)
    }
  ]
  ...

So I'm going to try to distill all of this down to what I think are the two functional requirements needed to get a decent dev set up…

  1. allow the user to hook into choo's render and wrap it, which will become the single place ud.defn(...) needs to be added to the codebase
  2. provide a way to trigger a render independent of state changes

The final piece of the puzzle is knowing when to call 2.. I'm still baffled by how the React one works. Does React use a render/digest loop? As far as I can see, browserify-hmr nor ud provide "events", but I'm sure we could hook into them somehow. The former logs a message to the console when it hot-swaps a module in.

Noting down @yoshuawuyts hot reload proof of concept here https://github.com/yoshuawuyts/hot-app-replacement/tree/choo

I think this is interesting but as far as I can see it

  1. saves app state
  2. reloads the entire bundle
  3. loads app state
  4. re-boots the entire app

?

I think it would be cool if we could leverage these community projects and also benefit from actual hot-swaps of updated modules, rather than sending the entire bundle. I can see the benefit of the entire reload, if any views have any state that's not stored in a model obviously this would help.

I'd favor the approach in hot-app-replacement because:

  1. it's simpler to maintain - browserify-hmr has proven to be buggy, and
    I've found it complicated to debug
  2. it's simpler to reason about - people can jump in and write their own
    implementations / contribute
  3. with choo apps it's an anti-pattern to have state live outside choo;
    should probably write a bit more in depth
    about this, but some widgets aside (hey maps) all state should merrily be
    handled by choo for transparancy
  4. watchify performs incremental rebuilds; the cost of then sending the
    full bundle over the loopback interface is negligable
  5. using the approach in hot-app-replacement also works for CSS, removing
    the need for some different CSS solution

Hah yeah so that's about it - obviously you're free to do what you want,
but this is my take on it (:

On Wed, Oct 26, 2016 at 6:59 PM Ben Gourley [email protected]
wrote:

Noting down @yoshuawuyts https://github.com/yoshuawuyts hot reload
proof of concept here
https://github.com/yoshuawuyts/hot-app-replacement/tree/choo

I think this is interesting but as far as I can see it

  1. saves app state
  2. reloads the entire bundle
  3. loads app state
  4. re-boots the entire app

?

I think it would be cool if we could leverage these community projects and
also benefit from actual hot-swaps of updated modules, rather than sending
the entire bundle. I can see the benefit of the entire reload, if any views
have any state that's not stored in a model obviously this would help.


You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
https://github.com/yoshuawuyts/choo/issues/295#issuecomment-256412122,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ACWlevJdm_Fim_PYDb7PCOyMZA_EJYznks5q34bUgaJpZM4KhBw_
.

@yoshuawuyts ok you have me convinced! Can we put together a list of todos in order to have this packaged up and ready to go?

I guess the only one reliant on you is the merge/publish of new barracks version? I'm happy to work on some of the other bits if we define what they are.

@bengourley

  • [ ] create a lil plugin that can be loaded into choo (choo-hmr perhaps?)
  • [ ] find a good Server Sent Events (SSE) implementation to send the bundles through; websockets usually requires compiling native code which is not great
  • [ ] patch bankai to use the server implementation

Something like this?

Sounds good @yoshuawuyts :D I might be able to get started on choo-mhr this week. It's going to be super useful to me on a project I'm working on.

[Edit: I hit cmd enter too soon and posted this before it was finished 😬 ]

So I've come up with a solution! Anyone want to try this out?

npm i --save choo-resume hot-rld

Attach the choo plugin. This saves the state on the window, and wraps the initial state to load from the window if it exists.

const app = require('choo')()
const resume = require('choo-resume')
app.start('#app-root')

Run the standalone SSE server. This watches the -b path on disk and notifies the client to hot-swap the <script> tag with source equal to -s.

./node_modules/.bin/hot-rld -b static/js/bundle.js -s /static/js/bundle.js

In your server rendered html output the client that connects to the SSE server.

const client = require('hot-rld/client')
const html = `<script>${client()}</script>`

Concerns

Since this reloads the entire app bundle, any side effects of the app, including timers, event listeners and other subscription-like behaviour will happen multiple times.

Crude example, causes a duplicate interval to be set up every reload:

subscriptions: [
  (send, done) => setInverval(() => console.log('boop'), 5000)
]

I can't think of a good way of solving this, other than sticking the state in local storage and doing a full page reload. But then it's not really a hot-reload, more like a snapshot/resume tool (_aside, that's not very hip and trendy, but it's still a useful and workable solution, perhaps we should run with that?_). This is why my gut feel previously was to go the hot-module-swap because then you can just swap out the immutable parts of choo and be guaranteed to not have side effects.

Thoughts❔❔❔

@bengourley oohhhh really digging this; might just work on top of this from https://github.com/yoshuawuyts/bankai/pull/86 - use the same client, but use bankai as the server. :grin:

Is there a way we could make this also work for CSS ? perhaps have different push channels that set the script and css individually? If we document the wire protocol then we could have compatible client and servers and like be super happy with all of it :sparkles:

@yoshuawuyts I decoupled the cli/standalone server from the sse route handler, so if you pull in require('hot-rld/server')(bundlePath, sourcePath) you get a fn with signature (req, res) => {} which you can mount wherever you like on a pre-existing server. You then just need to provide the address argument to the client script generator client('http://myhost:1234/path-to-where-sse-was-mounted')

As far as CSS is concerned, yeah sure we could namespace the sse events into channels. FYI I don't know if you dug deep into the code, but I'm only sending tiny payloads (i.e. the path of the file that needs replacing, not the source of the bundle) and the browser is re-requesting the entire bundle from wherever it was served initially.

I've published v1 of both https://github.com/bengourley/choo-resume and https://github.com/bengourley/hot-rld.

@yoshuawuyts hot-rld now supports css too.

Module reloading is part of bankai now too, couple with https://github.com/yoshuawuyts/choo-reload for SSE based reloading :sparkles:

Was this page helpful?
0 / 5 - 0 ratings