Opt-in to disable page reload when using HMR #418

Closed
gaearon opened this Issue Aug 21, 2014 · 54 comments

Projects

None yet

8 participants

@gaearon
Contributor
gaearon commented Aug 21, 2014

When HMR update fails, it forces a page reload.

This may be desirable for consistency, but most of the time this is caused by a simple syntax error, and I'd like to error to just appear in console. When I fix the error, another HMR update will come and "fix" the system anyway.

When react-hot-loader used a pitch loader, it just put HMR-updated require inside a try-catch.

Now that I refactored it to be a simple loader (no pitching), I no longer have the ability to catch syntax errors in the breaking module.

Can we have a way to disable page reloads altogether? I guess making it opt-in and logging something like "The system is in inconsistent state" is fine with me.

cc @syranide

@gaearon gaearon referenced this issue in gaearon/react-hot-loader Aug 21, 2014
Closed

In 0.4.0, page reloads on syntax error #21

@syranide
Contributor

👍

EDIT FROM SCRATCH: Thought it through a bit more and I think it largely comes down to if you're working with a stateful or stateless project. If you're working with a very stateful project you likely don't want it to reload automatically at all (at least I don't), whereas changes that can be hot-patched should be patched. But if you're working with a rather stateless project you want it to reload all the time, not reloading (when possible) is a bonus.

So I propose an option for whether or not "reloading" is at all allowed.

@sokra
Member
sokra commented Aug 21, 2014

What you really want is webpack/webpack-dev-server#42...

@syranide
Contributor

@sokra Definitely, but I'm pretty sure I also would want to be able to turn off reloading entirely, I do not want it to reload if I make an irrelevant change to a non-hot-replaceable file.

@gaearon
Contributor
gaearon commented Aug 22, 2014

I agree; it's also a matter of preference, as most of my team hate auto reloading. They're used to refreshing when they feel like it. For stateful project, auto reloading is quite irritating. For them, HMR is a nice bonus when it's possible, but not at the cost of constantly losing their state due to irrelevant edits.

@jhnns
Member
jhnns commented Aug 22, 2014

Agreed. HMR is great (e.g. for editing styles), but auto-reload is not always desired.

@sokra
Member
sokra commented Aug 22, 2014

Currently we cannot recover from an exception during hot update, but you can disable refreshing by writing you own HMR management code instead of using the prepared webpack/hot/dev-server (as workaround). Hey, integrate it with your app GUI.

@gaearon
Contributor
gaearon commented Aug 22, 2014

Bah! I never realized it's that small. OK :-)

@sokra
Member
sokra commented Aug 22, 2014

And it's mostly logging ^^

@jhnns
Member
jhnns commented Aug 22, 2014

Yep, I've already written my own 😀

@syranide
Contributor

For posterity, this is what I use for the moment:

function check() {
  module.hot.check(function(err, updatedModules) {
    if (updatedModules) {
      check();
    }
  });
}
window.onmessage = function(event) {
  if (event.data === 'webpackHotUpdate' && module.hot.status() === 'idle') {
    check();
  }
};
@gaearon
Contributor
gaearon commented Aug 22, 2014

I have similar code.

The only problem so far is that after one failed update (that I ignore inside my server), updates can't be applied anymore.

Apparently module.hot.status() becomes fail after the first update and I don't see how to make it recover.

@jhnns
Member
jhnns commented Aug 25, 2014

So you don't restart your app on failure (like with window.location.reload())?

@gaearon
Contributor
gaearon commented Aug 25, 2014

@jhnns

I don't—it's inconvenient to lose all state due to a simple typo. I may have style modifications or useful logs in Chrome devtools that will be lost because of that.

@jhnns
Member
jhnns commented Aug 26, 2014

That's true, but unless you have stateless views it's really hard to just swap out modules. Seems like one of your updated modules didn't provide an update handler via module.hot.accept and thus can't handle the update. For example take a look at the style-loader.

@gaearon
Contributor
gaearon commented Aug 26, 2014

@jhnns Not sure I'm following you.

unless you have stateless views it's really hard to just swap out modules

But it already works! This is precisely the problem react-hot-loader is solving (for React views).

I just don't want an occasional typo to either reload the page or disable HMR. I want the typo to be ignored till next HMR.

@sokra
Member
sokra commented Aug 26, 2014

It possible to catch the syntax errors in the accept handlers...

Example:

// Accepting another module
module.hot.accept("module", function() {
  try {
    require("module");
  } catch(e) {
    // Some fallback
  }
});
// Accepting self
module.hot.accept(function(err) {
  // Some fallback
});
@syranide
Contributor

@jhnns There are tons of cases where it will "fall out of sync" and error in practice, we're not "arguing" against that. But if I spot a minor mistake or grammatical error in my getTimeLeft-utility function while working on something entirely different, I don't want everything to reload, I just want it to keep running as-is.

If I add/change/remove a non-hot-updateable require, then sure, stop hot updates and tell me (please don't reload!). But if I make an insignificant change in a non-hot-replaceable file, please just ignore it and continue as normal.

To put it differently, assume that style-loader didn't support hot-replacing, I wouldn't want it to reload just because I added/modified/removed styles, I want it to keep working as-is without the style changes, because they're mostly irrelevant. When I want to preview the new styles, I reload manually.

@gaearon
Contributor
gaearon commented Aug 26, 2014

@sokra

The first example is how I used to do it before I rewrote the loader to avoid pitch stage. It allowed me to catch the error.

In your second example, how would you prevent HMR entering "fail" stage and refusing to accept future updates?

@sokra
Member
sokra commented Aug 26, 2014

It only goes into fail for unhandled exceptions. Once you have an exception handler it continues as normal...

@gaearon
Contributor
gaearon commented Aug 26, 2014

Ah, so there's a difference between accept() and accept(noop)? I didn't realize that..

@sokra
Member
sokra commented Aug 26, 2014

accept() goes into fail when the require throws.
accept(noop) ignores the exception from the require.
accept(function(err) { console.log(err) }) prints the exception but doesn't go into fail.
accept(function(err) { console.log(err); throw err }) prints the exception and does into fail.

@jhnns
Member
jhnns commented Aug 26, 2014

@sokra do your examples refer to "accepting itself" or "accepting another module"?
@syranide but if you don't want your app to be reloaded, why do you use HMR?

@gaearon
Contributor
gaearon commented Aug 26, 2014

@jhnns

I think these examples refer to accepting itself—I'll give it a try as it seems to solve my problem (although this issue is about webpack-wide support for disabling page reload).

As for why @syranide uses HMR, I think it's for the same reasons that I do: with react-hot-loader you get no-refresh hot reload for React components (like in this video). The whole point is avoiding refreshing.

@syranide
Contributor

@jhnns I see HMR as an awesome feature to avoid having to do lots of changes in the blind and then reload, not as a feature to reduce the time between visual updates (by avoiding reloads). I think neither is wrong, but the second makes no sense to my workflow for complex apps; I do not want to throw away all my app state because I made an insignificant change that I don't need to preview right now.

I.e. if I'm working on something mostly visual and stateless, please just reload for every change. But if I'm working on stateful application code then I absolutely don't want it to reload unless I say so, I accept the consequences of making potentially incompatible changes and reloading myself when necessary.

@gaearon
Contributor
gaearon commented Aug 26, 2014

@sokra Thanks, that's what I did: gaearon/react-hot-loader@f3485dc

@jhnns
Member
jhnns commented Aug 26, 2014

@syranide I think you're right... maybe that's a better default behavior than reloading the app. How should the feedback to the developer then look like

@sokra what do you think?

@syranide
Contributor

@jhnns It's covered by the "copy-pasteable" server code, that's where the reloading takes place. It's easy enough to disable reload and/or tailor to your own liking, but module.hot.status() === 'abort' so no further updates are accepted and applied. So if I make a change that cannot be hot-updated, then no further hot-updateable changes will be applied until reload. Simply being able to ignore non-hot-updateable changes sounds fine to me (if it breaks horribly when requires are added/moved/removed, that's fine, if it could be detected then that's great but not at all important IMHO).

@sokra sokra added a commit that closed this issue Aug 29, 2014
@sokra sokra allow to ignoreUnaccepted modules in HMR
fixes #418
b8fef9a
@sokra sokra closed this in b8fef9a Aug 29, 2014
@syranide
Contributor

@sokra You're the best! 👍

@sokra
Member
sokra commented Aug 29, 2014

Ok the HMR management API changed a bit:

module.hot.check([autoApply], callback);
module.hot.apply([options], callback);

So if you want to ignore unaccepted modules:

module.hot.check(function(err, updatedModules) {
  // err: Cannot check of update
  // updatedModules == null: No update available
  module.hot.apply({
    ignoreUnaccepted: true
  }, function(err, renewedModules) {
    // err: Active decline or error in accept/dispose handler
    // updatedModules - renewedModules: modules that were ignored
    //   (you propably want to tell the user about them)
    // renewedModules: all modules that were disposed
  });
});
@sokra
Member
sokra commented Aug 29, 2014

Feel free to try it with webpack@1.4.0-beta2...

@gaearon
Contributor
gaearon commented Aug 29, 2014

@syranide
Contributor

@sokra

Cannot not apply hot update to MyComponent.js: Cannot find module "./MyComponent"
... make another change ...
http://.../569a120f0f39423f7919.hot-update.json 404 (Not Found)

... and then future hot updates stop being applied. I'm using react-hot-loader. But perhaps this is to be expected?

PS. I would still like to be able to disable auto-reloads and hot updates being disabled after a non-hot updateable change... perhaps it's really easy for me to write a naive no-op/replace JavaScript hot loader that would work sufficiently well.

@chanon
chanon commented Sep 2, 2014

@sokra @gaearon @syranide Woah... such progress on this ... but does everything work as it should now?

After I submitted webpack/webpack-dev-server#42 and got my monkey patched version working I've been in react-hot-module-reloading bliss for a month making tons of progress on my React app so kind of forgot about checking in.

I'm not sure about this and other proposed changes and the updates to react-hot-loader, but the solution I described in the issue above works brilliantly with no problems at all. I can see instant changes whenever I save jsx files or less files. If I make any syntax errors in less/css/jsx/js files nothing bad happens / no whole page reload. The errors show up in the browser console. Then when I fix the syntax error the changes are updated immediately.

This was with react-hot-loader 0.2.0 so I was quite surprised to see it at 0.4.2 now.

I'm going to try the new stuff and maybe I'll put the changes I made into a commit that people can pull from if they want.

@gaearon
Contributor
gaearon commented Sep 2, 2014

@chanon I haven't yet updated hot loader to use this, will do!

@chanon
chanon commented Sep 2, 2014

@gaearon Cool! BTW thanks for react-hot-loader! webpack + react-hot-loader really speeds up iterating uis like nothing I've used before!

I've tried out the latest versions of webpack + webpack-dev-server + react-hot-loader.

react-hot-loader by itself actually behaves really well already.

The issues I've found regarding to hot reloading with the latest versions (without my fixes) are:

  1. A syntax error saved in a less/css file still causes a full page reload
  2. Syntax errors or errors such as requiring missing modules in .js files don't cause full page reloads (at least when I tried), but cause hot reloading to stop working for .js and .jsx files afterwards.

So I've ported my fixes from the old webpack/webpack-dev-server versions to the latest versions:

The first commit is a very simple one:
chanon/webpack-dev-server@1c0fdb2#diff-895656aeaccff5d7c0f56a113ede9662R39

It simply removes the reloadApp() call when there is a module compile error in client/index.js. This mirrors the behavior in live.js which doesn't reload on errors too. It fixes the full page reloads that occur whenever a module of any type fails.

It seems with the new react-hot-loader by itself, syntax errors in jsx files don't cause full page reloads anymore, but this still fixes the issue for css/less files (no 1. above) and possibly when errors in other files/loaders occur.

@chanon
chanon commented Sep 2, 2014

The second commit is in the HotModuleReplacementPlugin
chanon@57018bf

Here I listen to the 'failed-module' hook, if the compile failed then don't report the module changes to the client. This fixes the issues where hot reloading doesn't work after compilation failed and then later succeeds.

You can try making these changes to your local copy and see that everything works fine.

The changes in the second commit though, might not be entirely correct and @sokra would know best.

@gaearon
Contributor
gaearon commented Sep 3, 2014

@sokra

I tried using new accept/check behavior and couldn't get it to work :-(

How do I need to change this code:

'if (module.hot) {',
'  module.hot.accept(function (err) {',
'    if (err) {',
'      console.error("Cannot not apply hot update to " + ' + JSON.stringify(filename) + ' + ": " + err.message);',
'    }',
'  });',
'  module.hot.dispose(function () {',
'    var nextTick = require(' + JSON.stringify(require.resolve('next-tick')) + ');',
'    nextTick(__hotUpdateAPI.updateMountedInstances);',
'  });',
'}'

I tried

'if (module.hot) {',
'  module.hot.check(function (err, updatedModules) {',
'    if (err) {',
'      console.error(err);',
'      return;',
'    }',
'    if (updatedModules === null) {',
'      return;',
'    }',
'    module.hot.apply({ ignoreUnaccepted: true }, function (err, renewedModules) {',
'      console.log(updatedModules, renewedModules);',
'    });',
'  });',
'  module.hot.dispose(function () {',
'    var nextTick = require(' + JSON.stringify(require.resolve('next-tick')) + ');',
'    nextTick(__hotUpdateAPI.updateMountedInstances);',
'  });',
'}'

but it gives me

Uncaught Error: check() is only allowed in idle status 

If I add hot.status() === 'idle' around check call, it never seems to get inside check at all.

Obviously I'm misusing HMR API but I'm not sure how to do it right.

@sokra
Member
sokra commented Sep 3, 2014

I'll add an example...

@sokra sokra added a commit that referenced this issue Sep 3, 2014
@sokra sokra added hot-only dev-server #418 e43a76a
@sokra
Member
sokra commented Sep 3, 2014

just use the hot-only dev-server (webpack/hot/only-dev-server) instead of webpack/hot/dev-server. It should ignore unaccepted modules and display useful messages to console...
You don't need to change anything in the loaders...

This only affects unaccepted modules. You should still handle exceptions from accepted modules in your loader @gaearon

I also added a NoErrorsPlugin which make webpack to not emit assets when there is an error while compiling... (@chanon)

@chanon
chanon commented Sep 3, 2014

webpack/hot/only-dev-server used together with NoErrorsPlugin works perfectly. Thanks @sokra!

Without NoErrorsPlugin sometimes hot module reloading stops working for js/jsx files like syranide reported above. But with NoErrorsPlugin it seems to be very robust ... whatever I try, no compile errors can make HMR stop working. Very nice!

Only thing is with NoErrorsPlugin you get no errors in the browser console, but it is fine with me.

@gaearon
Contributor
gaearon commented Sep 3, 2014

@sokra So the recommended setup is to use only-dev-server with NoErrorsPlugin and nothing needs to be changed in react-hot-loader.. right? I'll try this later in the evening, just want to make sure I don't need to touch the loader. Can you clarify why I still need to catch errors from accepted modules? If there's a syntax error, it would become "unaccepted" anyway?

@sokra
Member
sokra commented Sep 3, 2014

A module containing module.hot.accept(); is accepted.
A module where all parents call module.hot.accept("dep", function() {...}) is also accepted.
Only accepted modules can be unloaded without destroying the application.
So an update bubble through the dependency tree until it finds accepted modules, and all affected modules are unloaded and the accepted module is reloaded.

A module is unaccepted if this process would bubble through the whole tree up to a entry point. This means it cannot be replaced without a full reload. The webpack/hot/only-dev-server just ignores this kind of modules and print some message telling you to reload the page to apply all updates.

With the react-hot-loader .jsx files are accepted. So every module that is only required by .jsx files can be hot updated (There is code that handles the update). So every update bubble to the nearest .jsx file(s) and everything is disposed. The affected .jsx files are reloaded (require). An unhandled exception orginating from this require call would cause the HMR to go into a unexpected state and it will go into fail. So it important to handle the exception in the loader.


The NoErrorsPlugin just puts webpack in a mode, where it'll only emit stuff when there is no error in compilation. This way you need to fix all syntax errors before the next update happens.


You can use both in combination or use both alone. All cases have their use cases.

i. e. because the react-hot-loader handles all require exceptions it may be ok to create bundles with missing modules because of syntax errors. These error can be catched at runtime.

@gaearon
Contributor
gaearon commented Sep 3, 2014

I see! Really appreciate you watching this thread and making changes to bring us hot reloading nirvana.

@gaearon
Contributor
gaearon commented Sep 4, 2014

I just tried NoErrorsPlugin with hot/only-dev-server and it is officially awesome.
Works like magic.
Thank you.

@gaearon
Contributor
gaearon commented Sep 18, 2014

@sokra Just wanted to thank you for this again. This hugely improved workflow for our designer who spends a lot of time in CSS and JSX and often makes small syntax errors. He rarely reloads the page now and iterates much faster. Also thanks to @syranide and @chanon for advocating this!

@sokra
Member
sokra commented Sep 18, 2014

@gaearon 😄

@gaearon gaearon referenced this issue in gaearon/react-hot-loader Sep 20, 2014
Closed

Stops live updating on WebPack error. #29

@KyleAMathews KyleAMathews added a commit to KyleAMathews/coffee-react-quickstart that referenced this issue Nov 28, 2014
@KyleAMathews KyleAMathews Don't reload on JS errors fb43cb3
@gaearon gaearon referenced this issue in gaearon/react-hot-loader Feb 10, 2015
Closed

What's the difference in the entry config? #73

@gaearon
Contributor
gaearon commented May 22, 2015

It seems like I've been doing a silly thing for a long time. If you use React Hot Loader with hot/only-dev-server, there's no need for NoErrorsPlugin. If you make a syntax error, it'll print it in the console, but the next reload will still work:

screen shot 2015-05-23 at 1 23 05

Which is awesome because with NoErrorsPlugin it just prints "Nothing hot updated." which can be misleading when you have an error. (Yeah I know I was misusing it, sorry!)

@gaearon
Contributor
gaearon commented May 22, 2015

Oh well, I was wrong. HMR breaks if you don't use NoErrorsPlugin and misspell a dependency name. Bummer. I'll probably create an issue because I'm not sure why it happens this way.

@sdtsui
sdtsui commented Aug 4, 2016

Just wanted to say this discussion is awesome, helped me understand webpack better, and saved me a few hours at work. Thanks all.

@koddo
koddo commented Oct 13, 2016 edited

The NoErrorsPlugin doesn't work for me. When I have an error in my code, the webpack still tries to send something to the browser, like http://localhost:3000/b206461c30a0ae51ec9c.hot-update.json.
But this file doesn't exist and I have historyApiFallback=true, so the server just sends the index.html, then the browser says Update check failed: SyntaxError: Unexpected token < in JSON at position 0 at XMLHttpRequest.request.onreadystatechange.
And after that the HMR stops working.

@koddo
koddo commented Oct 15, 2016

I'm not alone having the issue with the noErrorsPlugin: webpack/webpack-dev-server#655

When using the noErrorsPlugin, after making an error and then fixing it, module.hot.status() returns "check", when it should return "idle". So, basically it gets stuck somewhere, I can't figure out.

@mikeengland

@koddo sadly the fix for this issue only went into Webpack 2 beta. I do understand bug fixes going into 2 so it gets out the door more quickly but I am reliant on webpack v1 and so are a lot of people who feel more comfortable on a stable version.

@mikeengland

I haven't looked at the commit that fixes it in version 2, so I'm not sure how complex backporting the fix would be.

@koddo
koddo commented Oct 16, 2016

@mikeengland, I've seen it, and that would be hard, for sure. But my monkey patch works for me. Will use it until v2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment