React Hotloading in native Meteor is ready (i.e. no webpack)

@dortega, @priezz, yes, unfortuntely it isn’t obvious with manta what to do, so I forked mantra-sample-blog-app and added the necessary hotloading stuff. Most important is this changeset which shows all the changes needed from the original (and for those that want it, it’s possible to just clone clone gadicc/mantra-sample-blog-app-hot too).

For everyone else, just to be clear, this is for the upcoming “refactor” release of meteor-hmr, which uses the upcoming React Hot Loader v3 (currently in beta).

@gadicc, thanks for that. Do I understand right that I can use the updated hot-reload functionality only passing URLs of container modules to the routing routine (to be able to require them later), but not the containers themselves? I understand, that it is not considered as the good coding style, but I suppose there could be a case when several top-level containers are described in a single .jsx file. Passing containers’ names to the router will help in this case, but the whole construction will be too weird.

Sure. I’m not 100% sure I understood correctly. But I think these are the pertinent issues:

  1. Hot updates flow up the tree to an ancestor that can accept them. So if Routes.jsx > Container > X > Y and Y is updated, that will still work fine.

  2. require('path') is near identical to import X from 'path'. Currently Meteor transpiles import's to require()'s anyway via babel. There’s no way around this - but just for now. In Webpack 2 for example, imports work with hot loading, and Meteor will get this too in the future, I’m sure.

  3. Module exports are cached… so require('somePath') multiple times is cheap. We could require() earlier and give some local variable to the action() methods, but I think this would unnecessarily complicate what we are actually trying to achieve. But you could do this if you’re relying on passing a variable name around (e.g. there’s a plugin for react that figures out the variable name and passes it back to React).

Having written all that I think maybe the latest sentence is what you were getting at but if I missed anything please let me know :slight_smile: Heading out now but will be back later.

Well, it seems I was not clear enough :slight_smile:

1) In the examples we are passing an array of containers’ modules urls to the module.hot.accept(). Is it possible to pass not urls, but the variables (already imported somewhere)?

I have a routine, which helps me to create routes. I called it like this befor:

import Home from "/client/imports/pages/Home"
...
setRoutes( {
    "/": { container: Home },
} )

Currently I have rewritten the routine to call it this way:

setRoutes( {
    "/": { module: "/client/imports/pages/Home.jsx" },
} )

To be honest, I like first way more as it is not dependent on the place where Home is actually defined.

2) Imagine, you have containers.jsx, which has export const Container1() => ... and export const Container2() => .... Will both these containers be processed by module.hot.accept?

Ok, I’m with you now :slight_smile:

Unfortunately, no, we can’t pass variables (i.e. the imported modules’ exports) here… the hot.accept() is basically saying, if any of the given modules (specified by their relative module ids) are updated, run the given function to accept the change.

This is part of the hmr-runtime; when a new bundle comes through after an update/save, we know which modules have been updated (by their ids), and then work out if there is code available to accept the change. Even with Webpack2 in ES modules mode, where you don’t need the extra require(), you still need to specify the modules with string names.

You can keep things more similar to how you had them before as long as you have a paired module.hot() clause which requires the next version of the module, and that setRoutes() can replace existing keys. e.g.

import Home from "/client/imports/pages/Home"
...
setRoutes( {
    "/": { container: Home },
} )

if (modules.hot) {
  modules.hot.accept('/client/imports/pages/Home', function() {
    const NextHome = require('/client/imports/pages/Home');
    setRoutes( {
      "/": { container: NextHome }
    } );
  }
}

You should kind of consider the hot.accept() call itself to be outside the bounds of your regular app (just like import uses string module identifiers), and from inside the function you pass to hot.accept(), code works as usual.

That was kind of long but hope it makes things clearer.

Regarding point 2, yes; hot loading works per module (i.e. per file). But just like with import, you can do stuff with each modules exports indenpedently:

import { Container1, Container2 } from './containers.jsx';

if (module.hot)
module.hot.accept('./containers.jsx', function() {
  const NextContainer1 = require('./containers.jsx').Container1;
  const NextContainer2 = require('./containers.jsx').Container2;

  // I guess if you really wanted to, doesn't cover changed closures
  if (Container1.toString() !== NextContainer1.toString()) {
    console.log('Container1 was updated');
  }
});

But doing those kind of fine-grained comparisons isn’t very common. Don’t forget this is all just to save time during development… it’s generally ok if everything in the updated module flows to everything that imports that module.

Lastly, I hope it’s clear that I haven’t invented anything here… this is the same HMR pattern used by Webpack and Browserify. Oh and, I’m not 100% sure how Meteor’s client-side bundler will handle module id references that aren’t part of a require/import statement.

Happy to answer any more Qs :slight_smile:

1 Like

Thank you, @gadicc, for the great answer! Now it became absolutely clear (I believe). So, I just have to modify my routines and see, if it still works as expected :slight_smile:

1 Like

Still no luck… It does not seem that hot is getting the signal, that the file content was changed on the disk. I have added a simple check:

import { setHead, setRoutes } from "priezz_helpers"
import Home from "/client/imports/pages/Home"

setRoutes( {
    "/": { source: "/client/imports/pages/Home.jsx" },  /* I have to add '.jsx' extension explicitly */
} )

if( module.hot ) {
    console.log( "'hot' module available" )
    module.hot.accept( [ "/client/imports/pages/Home.jsx" ], () => console.log( "Home.jsx updated" ) )
}

I see, that hot module is loaded, however, when changing Home.jsx nothing happens until Meteor refreshes the page in a few seconds. Am I missing something?

P.S. import "react-hot-loader/patch" is in the first line of /client/index.js, which is the only JS file not in the imports.

P.P.S. setRoute routine is defined here https://gitlab.com/priezz/meteor-helpers/blob/master/src/routes.jsx. I use it adding "priezz_helpers": "git+https://gitlab.com/priezz/meteor-helpers" to dependencies. If someone likes this small wrapper, feel free to use, it will stay there.

Hey, @priezz, thanks for all the info! You should be able to tell straight away if you’re getting updates, they look like this (on the client console):

[gadicc:hot] Connected and ready.
[gadicc:hot] Updating ["/client/colors.js"]
[gadicc:hot] Skipping HCP after successful HMR

If nothing like that is happening, the first thing that comes to mind is issue #53. If you’re using Webstorm/Intellij or some other editor that does “safe writes” by default, we’re currently working on fixing that. You should disable this feature until we have it working.

Otherwise you can see if there is any more useful info be setting the HOT_DEBUG=1 environment variable. You should see something like this on the server console:

[gadicc:hot] Accelerator (Pof): gadicc:ecmascript-hot/compile-ecmascript-hot.processFilesForTarget(client/colors.js)
[gadicc:hot] Accelerator (Pof): Creating a bundle for 1 changed file(s)...

But if it wasn’t the safe write / atomic write issue, let’s rather create an issue for this on github so we can track everything properly. If it is that issue, I’ll make this clearer on the README until we have the fix working 100%.

@gadicc, no updates. Console stays intact after [gadicc:hot] Connected and ready.

I do not use IntelliJ based IDE and use locally installed C9 (with disabled .bak files) and VIM. Both give the same result. I will check with HOT_DEBUG=1 to create an issue in GH.

Well, after two days of trials @gadicc finally fixed the issue. Thank you so much!

For those, who are interested to use hot-reload with FlowRouter, I have updated the boilerplate https://gitlab.com/priezz/meteor-react-hot.git and the module behind it to provide automatic hot-reload for the routes (FlowRouter based). README is added as well. Now it is easy as:

setRoutes( {
    "/": { source: "/client/imports/pages/Home.jsx" },
}, module.hot )

Boilerplate features:

  • easy load all the dependencies (see package.json)
  • simplified routes definition (FlowRouter based)
  • simplified dynamic page head properties (title, meta, scripts and stylesheets) definition
  • automatic hot-reload of React components
  • PostCSS enabled, Stylus-like syntax is used by default (w/o installing Stylus itself)
1 Like

Nice! Awesome PostCSS plugin set too. I need to try it in my free time :slight_smile:

@priezz thanks for hanging in there and helping us get this fixed before the upcoming release! You encouraged me to create a Boilerplates & Examples doc which now lists the mantra-sample-app and your boilerplate there :slight_smile:

@juliancwirko when can we expect the juliancwirko:postcss-hot package? :slight_smile: Not sure if you saw the POC I did for css-modules here. Although you’d have to switch from being a minifier to being a build plugin and shipping the CSS as modules… a bit of a mish but probably worth it for future saved dev time.

Interesting, but yeah, maybe it is better to have standalone PostCSS support based on minifier and also CSS Modules + full PostCSS support in nathantreid:css-modules.

2 Likes

Nice to see it in the examples, thanks :slight_smile: I have to think, what can be done to enable hot reload for css as well.

Gadi, is it possible to track changes for all project files, not only .js/.jsx?

It needs to be done on a “per build plugin” process. We provide an ecmascript-hot that does .js/.jsx but most of the code is in the other packages. Once the v2 stable is out, I’d love to try get other build plugin authors to add hot support. There is a POC for meteor-css-modules though (you can see the “files changed” tab for more info, or the Build Plugin docs.

Will try that…

P.S. I have noticed that removing hot-code-push module from the project increases the refresh time for React. Maybe I am subjective though :slight_smile:

1 Like

Mmm, surprising observation, not quite sure why that would be :smiley: For me I quite like the combination of meteor-hmr and hot-code-push, because, if we can’t complete the HMR, the HCP comes through straight afterwards.

Sure, removing HCP is mostly useless when developing. However in some edge cases (i.e., developing single page website, not the app) it could be reasonable.

1 Like