Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🙋 Regular Browser Reload on File Save #289

Closed
mattdesl opened this issue Dec 15, 2017 · 44 comments · Fixed by #2676
Closed

🙋 Regular Browser Reload on File Save #289

mattdesl opened this issue Dec 15, 2017 · 44 comments · Fixed by #2676
Labels
HMR Hot Module Reloading 🙋‍♀️ Feature

Comments

@mattdesl
Copy link
Contributor

🙋

Hi there! I'd like to start exploring Parcel as a workflow for creative coding (WebGL, Canvas2D, etc). I'm trying to migrate some programs I've written for non-HMR workflow to this new HMR workflow, and I'm hitting huge performance issues since the code (which is doing things like creating a WebGL/Canvas context) was not designed to be re-run multiple times.

🎛 Configuration

Take a simple program like this:

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

document.body.appendChild(canvas);

🤔 Expected Behavior

I'd like to develop with the same way that a JS file runs/loads in the browser (i.e. once, not many times). If possible, I'd like a way to replace JavaScript HMR with a simple window.location.reload() functionality. However, other features (like CSS) should still use HMR / inject without hard reload.

😯 Current Behavior

Currently the above code, when saved several times, will create several canvas elements in the body.

💁 Possible Solution

A way of turning on/off regular hot reload. I am assuming this may already exist, but I couldn't find it, so perhaps it's more an issue of documentation?

@DeMoorJasper DeMoorJasper changed the title Regular Browser Reload on File Save 🙋 Regular Browser Reload on File Save Dec 15, 2017
@brandon93s
Copy link
Contributor

Give --no-hmr a try when you call parcel. It sounds like it may not be exactly what you're looking for, but it could be an improvement.

@mattdesl
Copy link
Contributor Author

Thanks! But that just turns off reloading altogether... 😆

@mattdesl
Copy link
Contributor Author

For now, I have a branch with the option --reload which will trigger a hard-reload on any non-CSS assets.

https://github.com/mattdesl/parcel/tree/feature/add-js-reload

@davidnagli
Copy link
Contributor

Honestly I don’t think this has any practical use cases outside of the one you described.

Is there anywhere else where somebody would actually want this option? If so, feel free to submit a PR with your add-js-reload branch.

@mattdesl
Copy link
Contributor Author

Honestly I don’t think this has any practical use cases outside of the one you described.

You mean, creating a new HTML element and adding it to the body? It's really one of the most basic things you can do with JS...

I am genuinely curious how I could change my application to better support HMR.

Here is another example: creating a loop with requestAnimationFrame:

window.requestAnimationFrame(render);
console.log('Starting loop...');

function render (time) {
  window.requestAnimationFrame(render);
  console.log('Rendering', time);
}

If you save the file twice you will end up with two requestAnimationFrame loops running simultaneously.

@davidnagli
Copy link
Contributor

Simple, just move the animation logic to a separate module, that way you get hot module replacement :)

I didn’t actually try this for myself, but something like this should work:

index.js

import draw from './draw'

window.requestAnimationFrame(render);
console.log('Starting loop...');

function render (time) {
  window.requestAnimationFrame(render);
  draw(time)
}

draw.js

export default function draw(time){
    console.log('Rendering', time);
    //...
}

@mattdesl
Copy link
Contributor Author

mattdesl commented Dec 17, 2017

@DeMoorJasper The following doesn't work – nothing changes when saving/editing the draw.js file:

import draw from './draw';

document.addEventListener("DOMContentLoaded", function(event) {
  window.requestAnimationFrame(render);
  console.log('Starting loop...');

  function render (time) {
    window.requestAnimationFrame(render);
    draw(time)
    console.log('Rendering', time);
  }
});

@davidnagli That doesn't work either, when I save the draw.js module it triggers a hot reload in my index.js and the loop is re-started (causing duplicate rAFs).

@DeMoorJasper
Copy link
Member

That's why i removed it, i realised it would completely disable HMR @mattdesl

@davidnagli
Copy link
Contributor

@mattdesl I was able to reproduce your issue, currently working on figuring it out.

@brandon93s
Copy link
Contributor

For the simple use case, you can do a bit of DOM management to replace instead of append on every change:

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

document.body.replaceChild(canvas, document.body.lastElementChild);

Saving this will result in the canvas reloading.

For the animation frame scenario, you can manage the request id:

if (window.currentAnimationFrameId){
    window.cancelAnimationFrame(currentAnimationFrameId)
}

window.currentAnimationFrameId = window.requestAnimationFrame(render);
console.log('Starting loop...');

function render (time) {
  window.currentAnimationFrameId = window.requestAnimationFrame(render);
  console.log('Rendering', time);
}

This isn't "automatic", but with a bit of boilerplate it allows the code to be HMR-compatible.

@jamiebuilds
Copy link
Member

I like the idea of a --reload flag, I much prefer it over HMR which I always opt out of.

@mattdesl
Copy link
Contributor Author

@thejameskyle Interesting, why do you opt out?

Personally I would rather --hmr be opt-in than on by default, since it’s far more magical than just a simple page reload.

@TennyZhuang
Copy link

+1 on --reload

@TennyZhuang
Copy link

TennyZhuang commented Dec 30, 2017

Hi everyone, if you really need regular reload but not HMR, you can try my fork https://github.com/TennyZhuang/parcel

just npm install git+https://git@github.com/TennyZhuang/parcel

Note that I have not make regular reload as an option now, so you can use it as same as the upstream, and there will always be a browser reload after every change.

I will try to make this as an option and make a merge request to the upstream if needed, @devongovett @brandon93s

@mattdesl
Copy link
Contributor Author

@TennyZhuang did you see my fork? It includes a ‘reload’ option.

I can submit it as a PR. Personally I think it should be the default, but I guess my goals are a little different than those of the Parcel maintainers.

@TennyZhuang
Copy link

TennyZhuang commented Dec 30, 2017 via email

@DeMoorJasper
Copy link
Member

@mattdesl feel free to submit a PR with it, would probably be much appreciated

@mattdesl
Copy link
Contributor Author

HMR is an important feature in parcel and faster than regular reload, I think HMR as default behavior is reasonable, but i really need regular reload in my three.js development.

It's not really faster – in my own tests I've found parcel reloading to be pretty much comparable to budo, which uses hard page reloads. With small to medium bundle sizes, budo seems to reload faster, and with huge bundles (several MBs), parcel seems faster.

Currently HMR is not even working in Parcel as pointed out earlier in this thread. I don't really understand how anybody is using this in production right now. 🤷

@mattdesl
Copy link
Contributor Author

mattdesl commented Dec 30, 2017

P.S. Submitted a PR. 😄

#443

@devongovett
Copy link
Member

Commenting here what I wrote on the PR so others can see. Seems to me like you could just hook into the existing HMR system to do a full page reload if you really wanted to.

if (module.hot) {
  module.hot.accept(function () {
    window.location.reload();
  });
}

However, I'm wondering why this is really needed? HMR is powerful because it allows you to maintain the state of your application across code changes, so you don't need to e.g. re-open a modal and click through 17 steps each time you make a change to the code.

@mattdesl
Copy link
Contributor Author

mattdesl commented Jan 1, 2018

@devongovett Yeah, that was the first thing I tried, although it feels like a hack. When I save a file, it triggers a hot module reload, so my code gets re-executed before the hot module replacement reloads the page.

This means any blocking code will slow down the reload cycle, and I end up with more memory usage during development. (Often the start of the application will create a WebGL context, generate geometries, and push features onto the GPU.)

However, I'm wondering why this is really needed? HMR is powerful because it allows you to maintain the state of your application across code changes, so you don't need to e.g. re-open a modal and click through 17 steps each time you make a change to the code.

I agree it can be very powerful, but only if the code is setup to work with it, and only in specific applications (e.g. React, Vue, etc). I've never been able to take advantage of application-wide HMR in a real WebGL project because of GL state, performance constraints, and things like that.

Anyways... as pointed out in this thread, hot module replacement is not working in Parcel, which is probably part of the reason some people are asking for a --reload option. Right now, any module change in your application will trigger a root-level reload, which means there is currently no clean way to avoid problems like window event listeners doubling up, simultaneous requestAnimationFrame loops, etc.

@devongovett
Copy link
Member

You could do something like this if you don't want the module to be re-executed:

if (module.hot) {
  module.hot.dispose(function () {
    window.location.reload();
  });
}

This will trigger the reload on module dispose rather than after the module has been re-executed.

You could also use that hook to store your state for later, and on accept restore it. HMR does take some work to get right, which is why things like react-hot-loader exist. Parcel is pretty much agnostic to that: it gives you hooks for when a module changes, it's up to you to decide what to do with that.

Right now, any module change in your application will trigger a root-level reload

That shouldn't be the case. The event starts at the module which changed, and bubbles up to the root. If you accept an update, the event stops bubbling up.

@mattdesl
Copy link
Contributor Author

mattdesl commented Jan 3, 2018

That shouldn't be the case. The event starts at the module which changed, and bubbles up to the root. If you accept an update, the event stops bubbling up.

Ok – this was never clear to me. It might be good to explain the system in the docs somewhere, rather than assume everybody is using React/Vue/etc.

So, how does one go about "setting up" HMR without copying the internals of react-hot-loader or similar modules? I really just want a basic app that reloads on file save (hence my PR), and I'd rather not add a deal of boilerplate to each file or have to carefully step around my code not to accidentally "Cmd + S" a certain file in case it duplicates window state.

The code here pretty much sums up the application I would like to build:
#289 (comment)

Or is it basically React-or-nothing?

@mattdesl
Copy link
Contributor Author

mattdesl commented Jan 3, 2018

P.S.

To illustrate the issue with reload and thread blocking, try this example:

index.js

import * as THREE from 'three';

console.log('Creating...');
const sphere = new THREE.SphereGeometry(1, 512, 512);
console.log('Done');

if (module.hot) {
  module.hot.accept(() => {
    // or use this instead of dispose()
    // window.location.reload();
  });

  module.hot.dispose(() => {
    window.location.reload();
  });
}

Whether using dispose or accept, the JS blocks and waits for the geometry to be generated. So you end up waiting twice as long: once before the reload is triggered, and once after the page actually reloads. :\

@pohy
Copy link

pohy commented Jan 26, 2018

@brandon93s Thanks, your solution works perfectly for me. Hence, I now have no need for the --reload option.

@jaredramirez
Copy link

jaredramirez commented Jan 27, 2018

+1 on wanting an --reload option (or something). When using elm-lang in fullscreen mode, an entirely new instance of the elm app is appended to the DOM on any .elm files saves.

Using

if (module.hot) {
  module.hot.dispose(() => {
    window.location.reload();
  });
}

works, but there still is an second where the new content flashes before the page reload occurs. Plus still having .css module replaced would be ideal

@mattdesl
Copy link
Contributor Author

@devongovett This makes sense to me, I think that's what webpack dev server does in hot mode.

@jamiebuilds
Copy link
Member

What about with asset types that implement hot module reloading themselves? Like it seems reasonable to make all CSS files support hot module reloading by default, but if you wanted to disable that behaviour for some reason, how could you?

@buckle2000
Copy link
Contributor

buckle2000 commented May 30, 2018

I found a hacky solution to reload immediately:

// put this at the top of your code
if (module.hot) {
    module.hot.dispose(() => {
        window.location.reload();
        throw 'whatever'
    })
}

Or...

if (module.hot) {
    module.hot.dispose(() => {
        window.location.reload();
    })
} else { do_something(); }

sampsyo added a commit to cucapra/gator that referenced this issue Jul 24, 2018
Parcel uses "hot module replacement" (HMR), which has quite intuitive
problems with `requestAnimationFrame`-based animation:
parcel-bundler/parcel#289

We now use a pretty ridiculous hack to cancel previous animation loops
when setting up new ones for our examples.
@teoxoy
Copy link

teoxoy commented Sep 15, 2018

I would like to have this functionality as well.

What if we automatically called window.location.reload() for you if the HMR event bubbled all the way to the top without any module calling module.hot.accept()? Then, by default, you'd get window reloading, but if you added HMR code to handle events yourself, it would not reload. No option needed! Thoughts?

Would this get implemented?

@nnmrts
Copy link

nnmrts commented Oct 24, 2018

I would love to have a --reload feature. Maybe I'm a bad developer and can't get my app to work perfectly with HMR, but never ever did a bundler send my fine working app in an infinite loop and crashed my browser tab when I save a file before. This is kinda frustrating.

@mattdesl
Copy link
Contributor Author

mattdesl commented Oct 24, 2018

For anyone who is looking for a zero-config tool for canvas/WebGL prototyping, I ended up building my own tool, canvas-sketch, that approaches (hot) reloading differently, in a way that is more suitable for requestAnimationFrame and so on.

@steve-king
Copy link

steve-king commented Oct 26, 2018

I'm running Parcel in a Django CMS setup. The HMR is fantastic for all my JS and CSS changes, however ParcelJS doesn't know anything about my Django HTML templates at this stage, nor is it possible or practical for Parcel to be used to bundle them in any way.

As such I don't get a reload when I update my templates. It would be great if I could just give Parcel the path to my templates directory and tell it to perform a hard reload whenever a change is detected there, but keep HMR support for the other stuff.

I know I should just implement browsersync for this sort of thing but it would seem a shame to add that as Parcel already handles almost everything I need/want for so little effort.

@steve-king
Copy link

Is it/could it be possible to initiate a hard reload over the websocket from a custom plugin?

Being able to execute custom code on the client side could be pretty powerful (I have no idea if this already exists though)

@AlexLomm
Copy link

AlexLomm commented Dec 3, 2018

Bump for the --reload feature. The js' hmr behavior is too brittle to even be the default reloading option, let alone the only one.

It can potentially lead to myriads of nasty bugs. Especially, in the hands of less-experienced developers. I'm not sure whether the hmr is worth such a price.

Thanks for the amazing work, btw.

@slikts
Copy link

slikts commented Dec 23, 2018

I second this; I often have to manually restart the dev server when developing since it just stops working. That's on top of it getting stuck in an endless loop as per #1317.

@devongovett devongovett added the HMR Hot Module Reloading label Jan 5, 2019
@paulosborne
Copy link

+1 for --reload option

@nnmrts
Copy link

nnmrts commented Jan 17, 2019

Guys, let's not get this to a 3130 level. @paulosborne, you would +1 it? Just hit the thumbs up button on the first post.

Thanks.

lambdalisue added a commit to lambdalisue/typescript-browser-sandbox that referenced this issue Feb 21, 2019
Because Parcel

- Could not serve static files
  parcel-bundler/parcel#1080
- Could not use native reload on HMR
  parcel-bundler/parcel#289
lambdalisue added a commit to lambdalisue/typescript-browser-sandbox that referenced this issue Feb 21, 2019
Because Parcel

- Could not serve static files
  parcel-bundler/parcel#1080
- Could not use native reload on HMR
  parcel-bundler/parcel#289
@zoutepopcorn
Copy link

I am using a workaround.

Because I'm getting an error when parcel is updating: 'parcelRequire' of undefined

// page reload on parcel hmr
window.onerror = function(e) {
    if(e == `Uncaught TypeError: Cannot read property 'parcelRequire' of undefined`) {
        console.log('yess')
        location.reload();
    }
};

@mischnic
Copy link
Member

mischnic commented Feb 23, 2019

So, should both of the suggested solutions be implemented?

  • --reload flag for cases where HMR doesn't work at all/causes an infinite loop, an explicit opt-out
  • Automatic reload when hmr accept bubbled up and wasn't accepted (would be somewhat similar to webpack's default, seems a better default anyway): 🙋 Regular Browser Reload on File Save #289 (comment)

@richyliu
Copy link

If you want to reload after file changes, you can run Parcel in watch mode, which compiles changes to another directory (default ./dist/) but does not run a server.

parcel watch index.html --no-hmr

Then run a live-server, which listens to file changes and reloads (assuming your output is to the ./dist/ directory)

live-server ./dist/

Here's what my npm start script looks like:

parcel watch ./src/index.html --no-hmr & live-server --port=8000 ./dist/

@devongovett
Copy link
Member

In #2676 which just landed in master, the default was changed to reload the page unless you call module.hot.accept() in your app. We found that HMR seems to fail more often than not, so made it an opt-in feature.

@metal3d
Copy link

metal3d commented Nov 14, 2021

For those using TypeScript:

const hot = (module as any)?.hot;
if (hot) {
  hot.dispose(() => {
    window.location.reload();
    throw "hotReload";
  });
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
HMR Hot Module Reloading 🙋‍♀️ Feature
Projects
None yet