-
Notifications
You must be signed in to change notification settings - Fork 46.2k
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
How should we set up apps for HMR now that Fast Refresh replaces react-hot-loader? #16604
Comments
There's some confusion. The new DevTools doesn't enable hot reloading (or have anything to do with reloading). Rather, the hot reloading changes Dan has been working on makes use of the "hook" that DevTools and React use to communicate. It adds itself into the middle so it can do reloading. I've edited the title to remove mention of DevTools (since it may cause confusion). |
As for the question about how the new HMR should be used, I don't think I know the latest thinking there. I see @gaearon has a wip PR over on the CRA repo: |
To clarify for readers, that PR is very outdated and not relevant anymore. I need to write something down about how Fast Refresh works and how to integrate it. Haven't had time yet. |
Okay, here goes. What Is Fast Refresh?It's a reimplementation of "hot reloading" with full support from React. It's originally shipping for React Native but most of the implementation is platform-independent. The plan is to use it across the board — as a replacement for purely userland solutions (like Can I Use Fast Refresh on the Web?Theoretically, yes, that's the plan. Practically, someone needs to integrate it with bundlers common on the web (e.g. Webpack, Parcel). I haven't gotten around to doing that yet. Maybe someone wants to pick it up. This comment is a rough guide for how you’d do it. What Does It Consist Of?Fast Refresh relies on several pieces working together:
You'll probably want to work on the integration part. I.e. integrating What's Integration Looking Like?There are a few things you want to do minimally:
At that point your app should crash. It should contain calls to Then you need to create a new JS entry point which must run before any code in your app, including if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') {
const runtime = require('react-refresh/runtime');
runtime.injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => type => type;
} This should fix the crashes. But it still won't do anything because these How you do that depends on your bundler. I suppose with webpack you could write a loader that adds some code before and after every module executes. Or maybe there's some hook to inject something into the module template. Regardless, what you want to achieve is that every module looks like this: // BEFORE EVERY MODULE EXECUTES
var prevRefreshReg = window.$RefreshReg$;
var prevRefreshSig = window.$RefreshSig$;
var RefreshRuntime = require('react-refresh/runtime');
window.$RefreshReg$ = (type, id) => {
// Note module.id is webpack-specific, this may vary in other bundlers
const fullId = module.id + ' ' + id;
RefreshRuntime.register(type, fullId);
}
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
try {
// !!!
// ...ACTUAL MODULE SOURCE CODE...
// !!!
} finally {
window.$RefreshReg$ = prevRefreshReg;
window.$RefreshSig$ = prevRefreshSig;
} The idea here is that our Babel plugin emits calls to this functions, and then our integration above ties those calls to the module ID. So that the runtime receives strings like As alternative to wrapping the module code, maybe there's some way to add a try/finally like this around the place where the bundler actually initializes the module factory. Like we do here in Metro (RN bundler). This would probably be better because we wouldn't need to bloat up every module, or worry about introducing illegal syntax, e.g. when wrapping Once you hook this up, you have one last problem. Your bundler doesn't know that you're handling the updates, so it probably reloads the page anyway. You need to tell it not to. This is again bundler-specific, but the approach I suggest is to check whether all of the exports are React components, and in that case, "accept" the update. In webpack it could look like something: // ...ALL MODULE CODE...
const myExports = module.exports;
// Note: I think with ES6 exports you might also have to look at .__proto__, at least in webpack
if (isReactRefreshBoundary(myExports)) {
module.hot.accept(); // Depends on your bundler
enqueueUpdate();
} What is You'll also want to manually register all exports because Babel transform will only call Finally, the const runtime = require('react-refresh/runtime');
let enqueueUpdate = debounce(runtime.performReactRefresh, 30); By this point you should have something working. NuancesThere are some baseline experience expectations that I care about that go into "Fast Refresh" branding. It should be able to gracefully recover from a syntax error, a module initialization error, or a rendering error. I won't go into these mechanisms in detail, but I feel very strongly that you shouldn't call your experiment "Fast Refresh" until it handle those cases well. Unfortunately, I don't know if webpack can support all of those, but we can ask for help if you get to a somewhat working state but then get stuck. For example, I've noticed that webpack's Similarly, we'd need to integrate it with an "error box" experience, similar to Let me know if you have any questions! |
Syntax errors / initialization errors should be "easy enough" to handle in some way before telling React to start a render, but how would rendering errors interact with error boundaries? If a rendering error happens, it'll trigger the closest error boundary which will snapshot itself into an error state, and there is no generic way to tell error boundaries that their children are magically possibly fixed after a live refresh. Does / should every refreshable component get its own error boundary for free, or does error handling work differently in the reconciler when the runtime support is detected on initialization? |
Is there any way to detect such components? As far as I understand - no. Except In any case - it seems to be that if one would throw all react-specific code from |
Using
I narrowed down that file to the simplest thing that causes it:
|
Fast Refresh code inside React remembers which boundaries are currently failed. Whenever a Fast Refresh update is scheduled, it will always remount them. If there are no boundaries, but a root failed on update, Fast Refresh will retry rendering that root with its last element. If the root failed on mount, |
File a new issue pls? |
I linked to my implementation, which itself uses
Can you make an example? I’m not following. Regardless, that’s something specific to the bundler. I made Metro do what I wanted. We can ask webpack to add something if we’re missing an API. The goal here is to re-execute as few modules as possible while guaranteeing consistency. We don’t want to bubble updates to the root for most edits.
Maybe, although I’d like to remove the top level container as well. I also want a tighter integration with the error box. Maybe that can still be called |
By the way I edited my guide to include a missing piece I forgot — the |
isLikelyComponentType(type) {
return typeof type === 'function' && /^[A-Z]/.test(type.name);
}, I would not feel safe with such logic. Even if all CapitalizedFunctions are React components almost always - many modules (of mine) has other exports as well. For example exports-for-tests. That's not a problem, but creates some unpredictability - hot boundary could be created at any point... or not created after one line change.
So - there would be cases when hot boundary shall be established, but would not, and there would be a cases when hot boundary would be established, but shall not. Sounds like old good unstable hot-reloading we both don't quite like :) There is one place where applying hot boundary would be not so unpredictable, and would be quite expected - a In other words - everything like you did in Metro, but with more limitations applied. Everything else, like linting rule to have such boundary established for any lazy loaded component, could be used to enforce the correct behaviour. Speaking of which - hot fast refresh would handle Lazy? Is it expected to have boundary from the other side of the |
Gave it a quick try to see the magic in the browser and it is so nice :) I did the simplest possible thing, i.e. hardcoding all the instrumentation code, so no webpack plugins there yet |
Just curious but for webpack, couldn't you just have a babel plugin to wrap the try/finally? Just want to be sure I'm not missing something before giving it a shot. |
The Babel plugin is not environment specific. I’d like to keep it that way. It doesn’t know anything about modules or update propagation mechanism. Those differ depending on the bundler. For example in Metro there’s no try/finally wrapping transform at all. Instead I put try/finally in the bundler runtime itself around where it calls the module factory. That would be ideal with webpack too but I don’t know if it lets you hook into the runtime like that. You could of course create another Babel plugin for wrapping. But that doesn’t buy you anything over doing that via webpack. Since it’s webpack-specific anyway. And it can be confusing that you could accidentally run that Babel plugin in another environment (not webpack) where it wouldn’t make sense. |
You can, by hooking into the The problem is getting a reference to React inside the For more details, check |
Here's a harebrained prototype I hacked together that does effectively what Dan outlined above. It's woefully incomplete, but proves out a lo-fi implementation in webpack: https://gist.github.com/maisano/441a4bc6b2954205803d68deac04a716 Some notes:
|
I assume you mean getting a reference to Refresh Runtime. In Metro I’ve solved this by doing |
@maisano I had to change a number of things, and ultimately I'm not seeing the .accept function called by webpack. I've tried both So I ended up patching webpack to call self-accepting modules, and that was the final fix. Here's the patch:
I know this goes against their API, which wants that to be an "errorCallback", I remember running into this specifically many years ago working on our internal HMR, and ultimately we ended up writing our own bundler. I believe parcel supports the "self-accepting" callback API. Perhaps it's worth us opening an issue on webpack and seeing if we can get it merged? @sokra |
So ... I further polished the plugin based on the work of @maisano : I tried to remove the need of a loader for injecting the hot-module code with webpack Another issue is that there are no simple ways (afaik) to detect JavaScript-like files in webpack - for example I also added error recovery (at least syntax error) based on comment from @gaearon in this 5-year old thread. Next is recovering from react errors - I suspect this can be done by injecting a global error boundary (kinda like The issue raised by @natew is also avoided - achieved by decoupling the |
This PR introduces fast refresh into the `@ssr-tools/core` lib. Since, the implementation of the fast refresh client is not trivial, I decided to use already existing tools: webpack-dev-server and react-refresh-webpack-plugin. To wire it with a custom app server, the app should use webpack-dev-server endpoint when fetching static assets (JS files, images etc…). The JS bundle exposed via webpack-dev-server is configured to update chunks related to specific client-side changes. This way you get near-instant feedback, that's very useful when adjusting some styles and seeing whether it feels good. It's worth noting that changes in the server-side only code do not trigger the hot reload. However, it causes the server to restart and use the latest version of the code afterwards. This way, you will be able to see the server-side changes after reloading the page (thus making a new request to the server). # Background It gets harder and harder to test today's applications because of their complexity. In the same time, the developers would like to have as fast as possible feedback once they change something in the code. The old flow of making a change in the code and refreshing the page is not sufficient anymore. A page refresh may clear some in-memory state of the app. That means the developer would have to wait for the network requests to reproduce the app's state. In some cases, they would even have to reproduce the state manually. That makes the development process ineffective and frustrating. # React Fast refresh Fast Refresh is a React feature that allows developers to get near-instant feedback for changes in the components. So that the developers doesn't have to rebuild the app every time they change something. It allows skipping state reproduction after page refresh, as well as long build times. The feature integrates with webpack via the community-baked plugin: https://github.com/pmmmwh/react-refresh-webpack-plugin Some internal details are explained here: facebook/react#16604 (comment)
@gaearon I'm trying to implement Fast Refresh into our website, a large application using RequireJS as a module loader. Without something like webpack's hot-reload API, I'm struggling to work out a mechanism to substitute in. Using a custom TS transformer (we're not using Babel currently) I'm wrapping each For each module modified locally, we have a custom function to create a new RequireJS context, load in the modified module file (including the above transforms) and patch it in by copying exports from the newly loaded module to the original module. After doing that, I'm then calling And the above works, for the specific module changed. But the refresh does not bubble up, so if I've just changed a file exporting a string, the component which imports that string won't see any changes applied. If we were using Webpack, we would look to see if the newly loaded component was a boundary, and Figuring out the parent module(s) to bubble to is complex; it would require me to maintain an inverted tree of the whole application in parallel outside RequireJS. But even if I do that, I don't have a way to specifically instruct fast-refresh to mark the parent as needing an update. I could hot-reload the parent so that the module source is re-executed, but that destroys state. As a "sledgehammer to crack a nut" solution, is there a way to simply mark the whole tree as pending update before calling the |
The calls are generated by the |
Why does it destroy the state? I don't think it should if the parent component is registered. |
@gaearon , @pmmmwh - it's probably a good time officially say goodby to our dear friend React-Hot-Loader. RHL still has 1M weekly downloads, which is hopefully 1/7th of |
Sadly, I am not an JS expert. Trying to migrate away from react-hot-loader.
What is the suggested upgrade path when moving over to babel with react-refresh (or whatever else is currently the supported solution) https://github.com/pmmmwh/react-refresh-webpack-plugin ? |
You dont need to change you runtime to support fast refresh. Just completely remove RHL and then install https://github.com/pmmmwh/react-refresh-webpack-plugin following it's pretty simple and short instructions - update babel config and add webpack plugin. |
Needs more testing, but seems to be working well Mainly following <https://github.com/pmmmwh/react-refresh-webpack-plugin>. Other useful resources: <facebook/react#16604>, <https://github.com/webpack/webpack-dev-middleware>, <https://github.com/webpack-contrib/webpack-hot-middleware>
Needs more testing, but seems to be working well Mainly following <https://github.com/pmmmwh/react-refresh-webpack-plugin>. Other useful resources: <facebook/react#16604>, <https://github.com/webpack/webpack-dev-middleware>, <https://github.com/webpack-contrib/webpack-hot-middleware>
This adds support for caching `fetch` responses in server components across HMR refresh requests. The two main benefits are faster responses for those requests, and reduced costs for billed API calls during local development. **Implementation notes:** - The feature is guarded by the new experimental flag `serverComponentsHmrCache`. - The server components HMR cache is intentionally independent from the incremental cache. - Fetched responses are written to the cache after every original fetch call, regardless of the cache settings (specifically including `no-store`). - Cached responses are read from the cache only for HMR refresh requests, potentially also short-cutting the incremental cache. - The HMR refresh requests are marked by the client with the newly introduced `Next-HMR-Refresh` header. - I shied away from further extending `renderOpts`. The alternative of adding another parameter to `renderToHTMLOrFlight` might not necessarily be better though. - This includes a refactoring to steer away from the "fast refresh" wording, since this is a separate (but related) [React feature](facebook/react#16604 (comment)) (also build on top of HMR). x-ref: #48481
Dan Abramov mentioned that Devtools v4 will be making
react-hot-loader
obsolete: https://twitter.com/dan_abramov/status/1144715740983046144?s=20I can't see any mention of HMR in the Devtools documentation, however; now that
react-hot-loader
has become obsolete (and with it, therequire("react-hot-loader/root").hot
method), how should we set up apps for HMR in:I'd be particularly interested in a migration guide specifically for anyone who's already set up HMR via
react-hot-loader
.Also, for HMR, does it matter whether we're using the standalone Devtools or the browser-extension Devtools?
The text was updated successfully, but these errors were encountered: