facebook / react

The library for web and native user interfaces.

Home Page:https://react.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How should we set up apps for HMR now that Fast Refresh replaces react-hot-loader?

shirakaba opened this issue · comments

Dan Abramov mentioned that Devtools v4 will be making react-hot-loader obsolete: https://twitter.com/dan_abramov/status/1144715740983046144?s=20

Me:
I have this hook:
require("react-reconciler")(hostConfig).injectIntoDevTools(opts);
But HMR has always worked completely without it. Is this now a new requirement?

Dan:
Yes, that's what the new mechanism uses. The new mechanism doesn't need "react-hot-loader" so by the time you update, you'd want to remove that package. (It's pretty invasive)

I can't see any mention of HMR in the Devtools documentation, however; now that react-hot-loader has become obsolete (and with it, the require("react-hot-loader/root").hot method), how should we set up apps for HMR in:

  • React DOM apps
  • React Native apps
  • React custom renderer apps

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?

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:
facebook/create-react-app#5958

commented

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.

commented

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 react-hot-loader).

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:

  • "Hot module replacement" mechanism in the module system.
    • That is usually also provided by the bundler.
    • E.g. in webpack, module.hot API lets you do this.
  • React renderer 16.9.0+ (e.g. React DOM 16.9)
    • Or react-reconciler@0.21.0 or higher for custom renderers
  • react-refresh/runtime entry point
  • react-refresh/babel Babel plugin

You'll probably want to work on the integration part. I.e. integrating react-refresh/runtime with Webpack "hot module replacement" mechanism.

What's Integration Looking Like?

⚠️⚠️⚠️ TO BE CLEAR, THIS IS A GUIDE FOR PEOPLE WHO WANT TO IMPLEMENT THE INTEGRATION THEMSELVES. THIS IS NOT A GUIDE FOR BEGINNERS OR PEOPLE WHO WANT TO START USING FAST REFRESH IN THEIR APPS. PROCEED AT YOUR OWN CAUTION! ⚠️⚠️⚠️

There are a few things you want to do minimally:

  • Enable HMR in your bundler (e.g. webpack)
  • Ensure React is 16.9.0+
  • Add react-refresh/babel to your Babel plugins

At that point your app should crash. It should contain calls to $RefreshReg$ and $RefreshSig$ functions which are undefined.

Then you need to create a new JS entry point which must run before any code in your app, including react-dom (!) This is important; if it runs after react-dom, nothing will work. That entry point should do something like this:

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 $RefreshReg$ and $RefreshSig$ implementations are noops. Hooking them up is the meat of the integration work you need to do.

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 "path/to/Button.js Button" when a component is being registered. (Or, in webpack's case, IDs would be numbers.) Don't forget that both Babel transform and this wrapping must only occur in development mode.

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 import with in try / finally.

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 isReactRefreshBoundary? It's a thing that enumerates over exports shallowly and determines whether it only exports React components. That's how you decide whether to accept an update or not. I didn't copy paste it here but this implementation could be a good start. (In that code, Refresh refers to react-refresh/runtime export).

You'll also want to manually register all exports because Babel transform will only call $RefreshReg$ for functions. If you don't do this, updates to classes won't be detected.

Finally, the enqueueUpdate() function would be something shared between modules that debounces and performs the actual React update.

const runtime = require('react-refresh/runtime');

let enqueueUpdate = debounce(runtime.performReactRefresh, 30);

By this point you should have something working.

Nuances

There 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 accept() API makes error recovery more difficult (you need to accept a previous version of the module after an error), but there's a way to hack around that. Another thing we'll need to get back to is to automatically "register" all exports, and not just the ones found by the Babel plugin. For now, let's ignore this, but if you have something that works for e.g. webpack, I can look at polishing it.

Similarly, we'd need to integrate it with an "error box" experience, similar to react-error-overlay we have in Create React App. That has some nuance, like the error box should disappear when you fix an error. That also takes some further work we can do once the foundation's in place.

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?

all of the exports are React components, and in that case, "accept" the update

Is there any way to detect such components? As far as I understand - no. Except export.toString().indexOf('React')>0, but it would stop working with any HOC applied.
Plus self accepting at the end of the file is not error-prone - the new accept handle would not be established, and next update would bubble to the higher boundary, that's why require("react-hot-loader/root").hot was created.

In any case - it seems to be that if one would throw all react-specific code from react-hot-loader, keeping the external API untouched - that would be enough, and applicable to all existing installations.

Using react-refresh/babel 0.4.0 is giving me this error on a large number of files:

ERROR in ../orbit-app/src/hooks/useStores.ts
Module build failed (from ../node_modules/babel-loader/lib/index.js):
TypeError: Cannot read property '0' of undefined
    at Function.get (/Users/nw/projects/motion/orbit/node_modules/@babel/traverse/lib/path/index.js:115:33)
    at NodePath.unshiftContainer (/Users/nw/projects/motion/orbit/node_modules/@babel/traverse/lib/path/modification.js:191:31)
    at PluginPass.exit (/Users/nw/projects/motion/orbit/node_modules/react-refresh/cjs/react-refresh-babel.development.js:546:28)

I narrowed down that file to the simplest thing that causes it:

import { useContext } from 'react'

export default () => useContext()
commented

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.

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, runtime.hasUnrecoverableErrors() will tell you that. Then you have to force a reload. We could handle that case later, I didn’t have time to fix it yet.

commented

Using react-refresh/babel 0.4.0 is giving me this error on a large number of files:

File a new issue pls?

commented

Is there any way to detect such components?

I linked to my implementation, which itself uses Runtime.isLikelyAReactComponent(). It’s not perfect but it’s good enough.

the new accept handle would not be established, and next update would bubble to the higher boundary

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.

it seems to be that if one would throw all react-specific code from react-hot-loader, keeping the external API untouched

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 react-hot-loader.

commented

By the way I edited my guide to include a missing piece I forgot — the performReactRefresh call. That’s the thing that actually schedules updates.

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.
What could break isLikelyComponentType test:

  • exported mapStateToProps (for tests, not used in a production code)
  • exported hook (and that's ok)
  • exported Class which might be not a react class (would not, but should)

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 thing or domain boundary, or a directory index, ie an index.js reexporting a "public API" from the Component.js in the same directory (not a Facebook style afaik).

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 import?

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

Kapture 2019-09-07 at 23 09 04

Repo here: https://github.com/pekala/react-refresh-test

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.

commented

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 compilation.mainTemplate.hooks.require waterfall hook. The previous invocation of it is the default body of the __webpack_require__ function, so you can tap into the hook to wrap the contents into a try/finally block.

The problem is getting a reference to React inside the __webpack_require__. It's possible, but might require some degree of reentrancy and recursion guards.

For more details, check MainTemplate.js and web/JsonpMainTemplatePlugin.js in the webpack source code. JsonpMainTemplatePlugin itself just taps into a bunch of hooks from MainTemplate.js so that's probably the "meat" that you need to tackle.

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:

  • react-dom is hardcoded here, so this would not work with custom renderers or sub-packages (e.g. react-dom/profiling).
  • I haven't looked too deeply into how all of webpack's template variants work, but the way I wrapped module execution is quite hacky. I'm not certain if this example would work if, say, one uses the umd library target.
commented

The problem is getting a reference to React inside the webpack_require. It's possible, but might require some degree of reentrancy and recursion guards.

I assume you mean getting a reference to Refresh Runtime.

In Metro I’ve solved this by doing require.Refresh = RefreshRuntime as early as possible. Then inside the require implementation I can read a property off the require function itself. It won’t be available immediately but it won’t matter if we set it early enough.

@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 .accept(module.i, () => {}) and .accept(() => {}) (self-accepting, except this doesn't work in webpack). The hot property is enabled, I see it come down and run through accepted modules.

So I ended up patching webpack to call self-accepting modules, and that was the final fix.

Here's the patch:

diff --git a/node_modules/webpack/lib/HotModuleReplacement.runtime.js b/node_modules/webpack/lib/HotModuleReplacement.runtime.js
index 5756623..7e0c681 100644
--- a/node_modules/webpack/lib/HotModuleReplacement.runtime.js
+++ b/node_modules/webpack/lib/HotModuleReplacement.runtime.js
@@ -301,7 +301,10 @@ module.exports = function() {
 				var moduleId = queueItem.id;
 				var chain = queueItem.chain;
 				module = installedModules[moduleId];
-				if (!module || module.hot._selfAccepted) continue;
+				if (!module || module.hot._selfAccepted) {
+					module && module.hot._selfAccepted()
+					continue;
+				}
 				if (module.hot._selfDeclined) {
 					return {
 						type: "self-declined",

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 :
https://github.com/pmmmwh/react-refresh-webpack-plugin
(I wrote it in TypeScript because I don't trust myself fiddling with webpack internals when I started, I can convert that to plain JS/Flow)

I tried to remove the need of a loader for injecting the hot-module code with webpack Dependency classes, but seemingly that will require a re-parse of all modules (because even with all functions inline, we still need a reference to react-refresh/runtime in somewhere).

Another issue is that there are no simple ways (afaik) to detect JavaScript-like files in webpack - for example html-webpack-plugin uses the javascript/auto type as well, so I hard-coded what seems to be an acceptable file mask (JS/TS/Flow) for loader injection.

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 AppWrapper of react-hot-loader), which will also tackle the error-box interface, but did not have the time to get to that just quite yet.

The issue raised by @natew is also avoided - achieved by decoupling the enqueueUpdate call and the hot.accpet(errorHandler) call.

@pmmmwh What timing! I just created a repo which built on/tweaked a little of the work I had shared in the gist.

I haven't gotten to error-handling in any case, though the plugin here is a bit more solid than the initial approach I had taken.

commented

Next is recovering from react errors - I suspect this can be done by injecting a global error boundary (kinda like AppWrapper of react-hot-loader), which will also tackle the error-box interface, but did not have the time to get to that just quite yet.

That should already work out of the box. No need for a custom error boundary or wrapping here.

Next is recovering from react errors - I suspect this can be done by injecting a global error boundary (kinda like AppWrapper of react-hot-loader), which will also tackle the error-box interface, but did not have the time to get to that just quite yet.

That should already work out of the box. No need for a custom error boundary or wrapping here.

@gaearon Strange. I tried throwing errors in rendering function components - if the error occurs in return, HMR works, but if it occurs somewhere else, it sometimes won't work.

@pmmmwh What timing! I just created a repo which built on/tweaked a little of the work I had shared in the gist.

I haven't gotten to error-handling in any case, though the plugin here is a bit more solid than the initial approach I had taken.

@maisano What should I say? I actually started working on this and got stuck with the dependency injection issue last weekend ... Your gist provided me the way out 🎉

commented

if the error occurs in return, HMR works, but if it occurs somewhere else, it sometimes won't work.

I would need more details about what you tried exactly and what you mean by “works” and “doesn’t work”.

There are many things that can go wrong if the module bundler integration isn’t implemented correctly (which is the topic or this thread). I would expect that there’s nothing in React itself that prevent recovery from errors introduced during edits. You can verify that it works in React Native 0.61 RC3.

@pmmmwh, @maisano the following check skips modules with components as named exports and no refresh boundary is established:

https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/master/src/loader/utils/isReactRefreshBoundary.ts#L23-L27

const desc = Object.getOwnPropertyDescriptor(moduleExports, key);
if (desc && desc.get) {
  // Don't invoke getters as they may have side effects.
  return false;
}

I don't know why this is needed in Metro, but in webpack getters just return the named exports and as far as I can tell there are no side effects. So it should be safe to remove.

@gaearon React.lazy components (e.g. code-split routes) are not being re-rendered. Is that by design? I can see that the refresh boundary is established but performReactRefresh() does not seem to do anything. Changes to lazy children refresh fine, so this isn't a big deal, but I'm wondering if we're doing something wrong...

lazy is a little state machine - it hold reference to the old component, and that reference has to be updated.
Now let's imagine it was, and now is referring to a brand new lazy object) - it will have to go thought loading phase again, and that would probably destroy all nested tree.

commented

I would expect lazy to work. Maybe something broke. I need to see a reproducing case.

Since there’s been a few prototypes, should we pick one and then move this discussion to its issues? And iterate there.

There is:
https://github.com/maisano/react-refresh-plugin
and:
https://github.com/pmmmwh/react-refresh-webpack-plugin
I've set up a fork of pmmmwh's plugin that works with webpack@5.0.0-alpha (also fixes named exports):
https://github.com/WebHotelier/webpack-fast-refresh

What about react-hot-loader?

react-hot-loader backported almost all features from fast refresh, but there are few historical and integrational moments, which are not letting backport all, and, honestly, there is no sense to reimplement them in "rhl" terms. So - let it retire.

I would need more details about what you tried exactly and what you mean by “works” and “doesn’t work”.

There are many things that can go wrong if the module bundler integration isn’t implemented correctly (which is the topic or this thread). I would expect that there’s nothing in React itself that prevent recovery from errors introduced during edits. You can verify that it works in React Native 0.61 RC3.

After a few tweaks, I can verify that it works.

However - it seems that the babel plugin wasn't working for classes. I have checked and this seems to happen regard-less of the implementation, as all injected code and react-refresh/runtime works properly. I am not sure if this is intended or if it is webpack specific, if it is the latter I can try to land a fix tomorrow. (I also tested this with only the metro preset, reproduce gist here)

I am kinda sure that it works for RN, but on my current machine I don't have an environment handy to test on RN so if you can point me to the implementation of babel plugin in metro or the transforms that would be really helpful.

Since there’s been a few prototypes, should we pick one and then move this discussion to its issues? And iterate there.

Maybe we can go here? Since my last comment I have ported the whole project into plain JS and added some fixes on update queuing. I haven't got to porting the plugin for webpack@5, but after reading the fork by @apostolos and the new HMR logic in webpack@next, the fixes should be straight-forward.

commented

Yes, Babel plugin won’t register classes. The intention is that this would happen at the module system level. Each export should be checked for being “likely” a React component. (A checking function is provided by the runtime.) If true, register the export, just like the Babel plugin would. Give it an ID like filename exports%export_name. This is what makes classes work in Metro, as Babel plugin won’t find them.

commented

In other words, since we can’t preserve class state anyway, we might as well entrust “locating” them to the module exports boundary instead of trying to find them inline in the source code with a transform. Exports should act as a “catch all” for components we didn’t find with the plugin.

Mailchimp started using a fork of the plugin I shared last. It's been fleshed out a tiny bit more and folks who've opted into using it seem to be quite happy. We're going to continue to iterate on it locally. I plan to remove the fork and publish updates upstream once it's a bit further along.

@gaearon Thoughts on adding a Symbol we can attach to things that we know are safe for refresh, but aren't components? For example we have a pattern like:

export default create({
  id: '100'
})

export const View = () => <div />

Where create just returns an object. I've patched it for now on my end, but we could easily add a symbol to the default export object there that indicates this is a safe file. Not sure the best pattern exactly.

Edit: I did realize this can go into the refresh implementation! I thought it may be better in the runtime but perhaps not. With so many different impls of the loader it may be nicer to have a standard way.

Let's forward 10 years. What your codebase looks like? Allow here, disallow there? How to keep these flags up to date? How to reason about? Like there are safe to update locations, and unsafe, you have to preserve, or can't properly reconcile by some reason. Which reasons in each case are valid reasons?

  • which symbols you will have more - about force allow reload, or force disallow reload
  • why you might want to lower update propagation boundary (ie accept the update on "this" module boundary), or want to raise it(ie accept the update on "that" module boundary)
  • what would happen if no boundaries would be set? Is it only a performance problem, or something more severe could happen?

Hi folks 👋 I'm looking to lend a hand here. Have we agreed on a single repo/effort?

Is it this repo shared by @pmmmwh?
https://github.com/pmmmwh/react-refresh-webpack-plugin

Or is it this repo shared by @maisano?
https://github.com/maisano/react-refresh-plugin

Looks like the one by @pmmmwh has been committed to more recently. Unless I hear otherwise I'm going to assume that's the one to focus on.

Implementation in Parcel 2 has started here: parcel-bundler/parcel#3654

For anyone looking for it, an implementation of React Refresh for Rollup projects using Nollup for development: https://github.com/PepsRyuu/rollup-plugin-react-refresh

Probably not the cleanest implementation, but it works.

For webpack solutions, it looks like there hasn't been any official release of the above plugins, so it seems the best HMR solution for react is Dan's library here still: https://github.com/gaearon/react-hot-loader

We just shipped Parcel 2 alpha 3 with support for Fast Refresh out of the box! Feel free to try it out. 😍 https://twitter.com/devongovett/status/1197187388985860096?s=20

🥳 added deprecation note to RHL 🥳

A recipe I've been using to to try this out on CRA apps using @pmmmwh's work in progress, react-app-rewired, and customize-cra:

npx create-react-app <project_dir> --typescript

npm install -D react-app-rewired customize-cra react-refresh babel-loader https://github.com/pmmmwh/react-refresh-webpack-plugin

Edit ./package.json:

  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },

Add ./config-overrides.js file:

// eslint-disable-next-line
const { addBabelPlugin, addWebpackPlugin, override } = require('customize-cra');
// eslint-disable-next-line
const ReactRefreshPlugin = require('react-refresh-webpack-plugin');

/* config-overrides.js */
module.exports = override(
  process.env.NODE_ENV === 'development'
    ? addBabelPlugin('react-refresh/babel')
    : undefined,
  process.env.NODE_ENV === 'development'
    ? addWebpackPlugin(new ReactRefreshPlugin())
    : undefined,
);

Enjoying the experience so far. Thanks for all the work from everyone involved!

Thanks @drather19 !

I created a repository based on your instruction, it works:
https://github.com/jihchi/react-app-rewired-react-refresh
If someone would like to give it a try and save some typing, feel free to clone the repo.


Please refer to https://github.com/pmmmwh/react-refresh-webpack-plugin/tree/master/examples/cra-kitchen-sink

AND ... v0.1.0 for Webpack is just shipped 🎉

@drather19 @jihchi
You guys might want to switch over to that version - it includes a more unified experience as well as a lot of bug fixes on the initial implementation.

@pmmmwh supports ts-loader + babel-loader ?

@pmmmwh supports ts-loader + babel-loader ?

I did test against TS with Babel only and it works, so if it doesn't work when you use ts+babel loaders please feel free to file an issue :)

@drather19 I tried cloning and running your repo but the dev server never starts up.

Environment,
OS - OSX 10.14.6
Node - v12.13.0
Yarn -1.19.2

@pmmmwh - FYI

react-app-rewired-react-refresh on  master is 📦 v0.1.0 via ⬢ v12.13.0
❯ yarn start
yarn run v1.19.2
$ react-app-rewired start | cat
ℹ 「wds」: Project is running at http://192.168.1.178/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /Users/seanmatheson/Development/temp/react-app-rewired-react-refresh/public
ℹ 「wds」: 404s will fallback to /index.html
Starting the development server...

@drather19 I tried cloning and running your repo but the dev server never starts up.

Environment,
OS - OSX 10.14.6
Node - v12.13.0
Yarn -1.19.2

@pmmmwh - FYI

react-app-rewired-react-refresh on  master is 📦 v0.1.0 via ⬢ v12.13.0
❯ yarn start
yarn run v1.19.2
$ react-app-rewired start | cat
ℹ 「wds」: Project is running at http://192.168.1.178/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /Users/seanmatheson/Development/temp/react-app-rewired-react-refresh/public
ℹ 「wds」: 404s will fallback to /index.html
Starting the development server...

This is fixed in the plugin's master branch, and will be released tomorrow.

I managed to get @pmmmwh 's webpack plugin working with a TypeScript React app using babel. However, the incremental builds take around 12 seconds instead of ~2 seconds with just ts-loader. I'm going to keep playing with this to see if I'm missing something on the babel config side which makes the performance closer, but for now it's a wash compared to ts-loader and full refreshes.

commented

@IronSean Please report it in the repo of that plugin? This doesn't sound normal.

I'm going to keep playing with this to see if I'm missing something on the babel config side which makes the performance closer, but for now it's a wash compared to ts-loader and full refreshes.

Mind posting your config/setup there? I won't be able to figure out the issues without more context.

@pmmmwh I opened this issue to move the discussion to your repo once I confirmed it was indeed your plugin making the difference:
pmmmwh/react-refresh-webpack-plugin#20

Will react-refresh (React Fast Refresh?) work with Preact, or is react-hot-loader the long-term solution for Preact?

commented

@Jumblemuddle that depends on Preact but they should be able to integrate with Fast Refresh if they want to.

For CRA folks wanting to run with Fast Refresh, I have had better luck with craco (vs. react-app-rewired+customize-cra) now via the following craco.config.js:

// eslint-disable-next-line
const { whenDev } = require('@craco/craco');
// eslint-disable-next-line
const ReactRefreshPlugin = require('react-refresh-webpack-plugin');

module.exports = {
  webpack: {
    configure: webpackConfig => {
      if (process.env.NODE_ENV === 'development') {
        webpackConfig.module.rules.push({
          test: /BabelDetectComponent\.js/,
          use: [
            {
              loader: require.resolve('babel-loader'),
              options: {
                plugins: [require.resolve('react-refresh/babel')],
              },
            },
          ],
        });
        webpackConfig.module.rules.push({
          test: /\.[jt]sx?$/,
          exclude: /node_modules/,
          use: [
            {
              loader: require.resolve('babel-loader'),
              options: {
                presets: [
                  '@babel/react',
                  '@babel/typescript',
                  ['@babel/env', { modules: false }],
                ],
                plugins: [
                  '@babel/plugin-proposal-class-properties',
                  '@babel/plugin-proposal-optional-chaining',
                  '@babel/plugin-proposal-nullish-coalescing-operator',
                  'react-refresh/babel',
                ],
              },
            },
          ],
        });
      }
      return webpackConfig;
    },
    plugins: [
      ...whenDev(
        () => [new ReactRefreshPlugin({ disableRefreshCheck: false })],
        [],
      ),
    ],
  },
};

In particular, adding webpackConfig.optimization.runtimeChunk = false; will let you add/remove hooks and still gracefully fast refresh.

Enjoying the improved experience even more now. Thanks to @mmhand123 for the tip via pmmmwh/react-refresh-webpack-plugin#25! (<-- resolved!)

Based on the suggestion by @drather19 I have published a customize-cra plugin to make it easier. See esetnik/customize-cra-react-refresh.

commented

Thanks to @drather19 I slightly modify the code now it can work in a yarn workspace monorepo setup.

First, install the following in the sub-packages you want to enable fast refresh:

"@craco/craco": "^5.6.3", "@pmmmwh/react-refresh-webpack-plugin": "^0.2.0", "webpack-hot-middleware": "^2.25.0"

Then add this to craco.config.js:

;(function ForbidCRAClearConsole() {
    try {
        require('react-dev-utils/clearConsole')
        require.cache[require.resolve('react-dev-utils/clearConsole')].exports = () => {}
    } catch (e) {}
})()

const { whenDev } = require('@craco/craco')
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin')

module.exports = {
    webpack: {
        configure: webpackConfig => {
            whenDev(() => {
                // Work around monorepo setup when using yarn workspace hoisted packages
                // without the need to list 'babel-loader' and 'babel-preset-react-app' as
                // dependencies to avoid duplication since 'react-scripts' already has them.
                const reactLoaderConfig = webpackConfig.module.rules
                    .find(x => Array.isArray(x.oneOf))
                    .oneOf.find(
                        x =>
                            x.options &&
                            x.options.presets &&
                            x.options.presets.some(p => p.includes('babel-preset-react-app')) &&
                            x.loader &&
                            typeof x.loader.includes === 'function' &&
                            x.loader.includes('babel-loader') &&
                            x.test &&
                            typeof x.test.test === 'function' &&
                            x.test.test('x.tsx') &&
                            x.test.test('x.jsx'),
                    )

                if (reactLoaderConfig) {
                    webpackConfig.module.rules.push({
                        test: /BabelDetectComponent\.js/,
                        use: [
                            {
                                loader: reactLoaderConfig.loader,
                                options: {
                                    plugins: [require.resolve('react-refresh/babel')],
                                },
                            },
                        ],
                    })

                    webpackConfig.module.rules.push({
                        test: /\.[jt]sx?$/,
                        exclude: /node_modules/,
                        use: [
                            {
                                loader: reactLoaderConfig.loader,
                                options: {
                                    presets: reactLoaderConfig.options.presets,
                                    plugins: [require.resolve('react-refresh/babel')],
                                },
                            },
                        ],
                    })
                } else {
                    console.error('cannot find react app loader')
                }

                // console.debug(require('util').inspect(webpackConfig.module.rules, { colors: true, depth: null }))
            })

            return webpackConfig
        },
        plugins: [whenDev(() => new ReactRefreshPlugin({ disableRefreshCheck: false }))].filter(Boolean),
    },
}

@gaearon Do we expect Fast Refresh to become available in the CRA by default at some point in time?
if so what is required for that?

commented

Some amount of work is required for that :-) which is currently being done.

if use HMR functions will be called ? for example componentDidMount.
I use react-proxy and componentDidMount will be called.
And react 15.X can use Fast Refresh?

  • componentDidMount will be called. As well as unmount - classes would be reloaded in full.
  • and it is a good time to stop using react-proxy. Well, you should stopped a few years ago probably.
  • 15.X? - absolutely not. Fast Refresh is a part of react, and thus exists only in a modern versions.

so we should use Fast Refresh or react-hot-loader to replace react-proxy?
Is there a way to prevent functions(componentDidMount) from executing for HMR? - it will call method to get new data.

How should i use react-hot-loader in JIT ? - Browser real-time compilation

  • so we should use Fast Refresh or react-hot-loader to replace react-proxy?

Try fast refresh first, then RHL

  • Is there a way to prevent functions(componentDidMount) from executing for HMR? - it will call method to get new data.

(use hooks...), do not rely on component lifeCycle, fetch the data when needed. Try react-query, swr or other solutions.

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.

As of today, that PR is still open. It would be nice if only relevant PRs that still have a chance to be merged would be kept open to have a better overview. If you're just keeping them as a reference, i'd recommend moving stuff to a branch, tag or different repository.

I keep getting Error: [React Refresh] Hot Module Replacement (HMR) is not enabled! React Refresh requires HMR to function properly. I've followed the documentation but seems like I might've missed something?

commented

I keep getting Error: [React Refresh] Hot Module Replacement (HMR) is not enabled! React Refresh requires HMR to function properly. I've followed the documentation but seems like I might've missed something?

@silkfire I'm assuming you're using the webpack plugin. If yes, please file your question in the webpack plugin repo: https://github.com/pmmmwh/react-refresh-webpack-plugin/.

As of today, that PR is still open. It would be nice if only relevant PRs that still have a chance to be merged would be kept open to have a better overview. If you're just keeping them as a reference, i'd recommend moving stuff to a branch, tag or different repository.

I appreciate your suggestion, but with thousands of unread notifications it can sometimes be difficult for me to remember to revisit old PRs. I trust the Create React App repository maintainers to do the right thing and close if they consider it not useful anymore.

commented

I'm going to close this.

We have https://github.com/pmmmwh/react-refresh-webpack-plugin/ as a reference implementation for webpack.
And #16604 (comment) explains how to make a custom integration.

I keep getting Error: [React Refresh] Hot Module Replacement (HMR) is not enabled! React Refresh requires HMR to function properly. I've followed the documentation but seems like I might've missed something?

It seems like you haven't enabled webpack HMR. For further help please file an issue in the plugin's repo.

As Hot Replacement is now a part of React - should it have a separate place in the React documentation, pointing to the additional libraries to be used with particular bundlers and platforms, as well as explaining some still existing gotchas, like with self-updating css modules.

Information like this should not be buried in github issues and blog posts.

@theKashey it's in React, but the react-dom implementation is only experimental, for one.
Also, there's a fast refresh implementation that will be bundled with create-react-app, but it hasn't been released yet: pmmmwh/react-refresh-webpack-plugin#7. Perhaps it will be in the next react-scripts version.

So probably the React team currently do not feel it right to talk about Fast Refresh for react-dom in this experimental phase yet.

commented

it's in React, but the react-dom implementation is only experimental, for one.

To be clear, the implementation in react-dom itself is stable, just like in React Native. It's just that the integrations are not all stable.

should it have a separate place in the React documentation, pointing to the additional libraries to be used with particular bundlers and platforms, as well as explaining some still existing gotchas, like with self-updating css modules.

This sounds reasonable. I'd be happy to take a PR adding it to the Advanced Guides section, maybe based on similar RN page.

@gaearon
My react app is okay with some styled component changes and correctly applying those changes without any problems.
However, when I change some code in a Redux's reducer, whole app is hard-refreshed and loses all the redux states.
Do I need to use some other libraries like redux-persist to save the current state along with react-fast-refresh?

We've went a full circle and here we go again 😅

That's how low level HMR is working, and is outside of fast-refresh responsibility. Please refer to redux or webpack docs

We've went a full circle and here we go again 😅

That's how low level HMR is working, and is outside of fast-refresh responsibility. Please refer to redux or webpack docs

Would you link the full circle reference?

@jwchang0206 Make sure you have code like this in your store.

Would you link the full circle reference?

The same questions were asked for React Hot Loader. The same answers were given. We are in the beginning of a new cycle.

@jwchang0206 Look at redux-reducers-injector, a small library I wrote for addressing this issue.
It will allow you to support reducers reloading with hot reloading.
Make sure you follow the redux principles of immutability in your reducers and it will work smooth 💯
And if you are using sagas, you can use redux-sagas-injector.

@gaearon I'm a bit confused by the use of window. It doesn't look to me as if it is really necessary because the implementation is swapped out? What's the point of that?

var prevRefreshReg = window.$RefreshReg$; // these are dummies
var prevRefreshSig = window.$RefreshSig$; // these are dummies
var RefreshRuntime = require('react-refresh/runtime');

window.$RefreshReg$ = (type, id) =>{ /*...*/ }
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;

try {
  // ...
} finally {
  window.$RefreshReg$ = prevRefreshReg; // these are dummies again
  window.$RefreshSig$ = prevRefreshSig; // these are dummies again
}

I have my own custom bundler and I'm the process of implementing this but I can't see why that would be an absolute must or what the point of it would be... initially I suspected some memory usage/leakage optimization but these are just calls forwarded to the RefreshRuntime...

@leidegre I can’t comment on the decision to set $RefreshSig$ on the window object, but the coupling to a browser environment gave me problems when consuming Fast Refresh in React NativeScript. @pmmmwh came to the rescue by adapting his Fast Refresh Webpack plugin to overcome Fast Refresh’s coupling to the browser (issues encountered and overcome were discussed in this thread: https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/79). I wonder if the approach used would be of any use to you in your custom bundler’s integration of Fast Refresh.

My bundler is mostly a wrapper around the TypeScript compiler. The implementation is mostly this, adapted from the react-refresh/babel visitor.

This is just a simple thing that works but it's not as complete as the react-refresh/bable visitor.

import ts = require("typescript")

import { IndexModule } from "./registry"

/** Enables the use of `react-refresh` for hot reloading of React components. */
export function hotTransform(m: IndexModule, hot: boolean) {
  // see https://github.com/facebook/react/issues/16604#issuecomment-528663101
  return (ctx: ts.TransformationContext) => {
    return (sourceFile: ts.SourceFile) => {
      const refreshRuntime = ts.createUniqueName("ReactRefreshRuntime")

      const createSignatureFunctionForTransform = ts.createPropertyAccess(
        refreshRuntime,
        "createSignatureFunctionForTransform"
      )

      const register = ts.createPropertyAccess(refreshRuntime, "register")

      let hasComponents = false

      function visitor(node: ts.Node): ts.VisitResult<ts.Node> {
        if (ts.isFunctionDeclaration(node)) {
          if (_hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
            // assert component naming convention

            if (node.name === undefined) {
              console.warn("unsupported export of unnamed function in ...")
              return node
            }

            const name = node.name
            if (!_isComponentName(name.text)) {
              console.warn(
                `warning: unsupported export '${name.text}' in ${m.path} (${m.id}) does not look like a function component, component names start with a capital letter A-Z. TSX/JSX files should only export React components.`
              )
              return node
            }

            if (!hot) {
              return node // opt-out
            }

            hasComponents = true

            let hookSignatureString = ""

            function hookSignatureStringVisitor(
              node: ts.Node
            ): ts.VisitResult<ts.Node> {
              const hookSig = _getHookSignature(node)
              if (hookSig !== undefined) {
                if (0 < hookSignatureString.length) {
                  hookSignatureString += "\n"
                }
                hookSignatureString += hookSig
              }
              return node
            }

            // update function body to include the call to create signature on render

            const signature = ts.createUniqueName("s")

            node = ts.visitEachChild(
              node,
              (node) => {
                if (ts.isBlock(node)) {
                  return ts.updateBlock(
                    ts.visitEachChild(node, hookSignatureStringVisitor, ctx),
                    [
                      ts.createExpressionStatement(
                        ts.createCall(signature, undefined, [])
                      ),
                      ...node.statements,
                    ]
                  )
                }
                return node
              },
              ctx
            )

            const signatureScope = ts.createVariableStatement(
              undefined,
              ts.createVariableDeclarationList(
                [
                  ts.createVariableDeclaration(
                    signature,
                    undefined,
                    ts.createCall(
                      createSignatureFunctionForTransform,
                      undefined,
                      undefined
                    )
                  ),
                ],
                ts.NodeFlags.Const
              )
            )

            const createSignature = ts.createExpressionStatement(
              ts.createCall(signature, undefined, [
                name,
                ts.createStringLiteral(hookSignatureString),
              ])
            )

            const registerComponent = ts.createExpressionStatement(
              ts.createCall(register, undefined, [
                name,
                ts.createStringLiteral(m.path + " " + name.text),
              ])
            )

            return [signatureScope, node, createSignature, registerComponent]
          }
        }

        if (!hot) {
          // if hot reloading isn't enable, remove hot reloading API calls
          if (ts.isExpressionStatement(node)) {
            const call = node.expression
            if (ts.isCallExpression(call)) {
              if (
                _isPropertyAccessPath(
                  call.expression,
                  "module",
                  "hot",
                  "reload"
                )
              ) {
                return undefined
              }
            }
          }
        }

        return node
      }

      sourceFile = ts.visitEachChild(sourceFile, visitor, ctx)

      if (hot && hasComponents) {
        let reactIndex = sourceFile.statements.findIndex((stmt) => {
          if (ts.isImportEqualsDeclaration(stmt)) {
            const ref = stmt.moduleReference
            if (ts.isExternalModuleReference(ref)) {
              const lit = ref.expression
              if (ts.isStringLiteral(lit)) {
                return lit.text === "react"
              }
            }
          }
          return false
        })

        if (reactIndex === -1) {
          console.warn(`cannot find import React = require('react') in ...`)
          reactIndex = 0
        }

        // insert after

        sourceFile = ts.updateSourceFileNode(sourceFile, [
          ...sourceFile.statements.slice(0, reactIndex + 1),
          ts.createImportEqualsDeclaration(
            undefined,
            undefined,
            refreshRuntime,
            ts.createExternalModuleReference(
              ts.createStringLiteral("react-refresh/runtime")
            )
          ),
          ...sourceFile.statements.slice(reactIndex + 1),
          ts.createExpressionStatement(
            ts.createCall(
              ts.createPropertyAccess(
                ts.createPropertyAccess(
                  ts.createIdentifier("module"),
                  ts.createIdentifier("hot")
                ),
                ts.createIdentifier("reload")
              ),
              undefined,
              undefined
            )
          ),
          ts.createExpressionStatement(
            ts.createBinary(
              ts.createPropertyAccess(
                ts.createIdentifier("globalThis"),
                ts.createIdentifier("__hot_enqueueUpdate")
              ),
              ts.createToken(ts.SyntaxKind.AmpersandAmpersandToken),
              ts.createCall(
                ts.createPropertyAccess(
                  ts.createIdentifier("globalThis"),
                  ts.createIdentifier("__hot_enqueueUpdate")
                ),
                undefined,
                undefined
              )
            )
          ),
        ])
      }

      return sourceFile
    }
  }
}

function _hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
  const modifiers = node.modifiers
  if (modifiers !== undefined) {
    for (let i = 0; i < modifiers.length; i++) {
      if (modifiers[i].kind === kind) {
        return true
      }
    }
  }
  return false
}

function _isComponentName(name: string): boolean {
  // ^[A-Z]
  const ch0 = name.charCodeAt(0)
  return 0x41 <= ch0 && ch0 <= 0x5a
}

function _isPropertyAccessPath(
  node: ts.Expression,
  ...path: ReadonlyArray<string>
): node is ts.PropertyAccessExpression {
  for (let i = 0; i < path.length; i++) {
    if (ts.isPropertyAccessExpression(node)) {
      if (!(node.name.text === path[path.length - (i + 1)])) {
        return false
      }
      node = node.expression
    }
  }
  return true
}

function _getHookSignature(node: ts.Node): string | undefined {
  if (ts.isExpressionStatement(node)) {
    const call = node.expression
    if (ts.isCallExpression(call)) {
      const prop = call.expression
      if (ts.isPropertyAccessExpression(prop)) {
        const text = prop.name.text
        if (text.startsWith("use") && 3 < text.length) {
          // todo: add additional checks and emit warnings if the hook usage looks non standard

          return text
        }
      }
    }
  }
  return undefined
}

I wasn't sure how to use createSignatureFunctionForTransform at first but it's just a factory function that creates a little state machine for each component. So you call it once for each function with the static hook signature (which is just an opaque value, akin to a hash). You then call it from render for it to finish it's setup work.

It changes something like this:

import React = require("react")

export function App() {
  const [state, setState] = React.useState(0)

  return (
    <React.Fragment>
      <p>
        Click Count !!!<strong>{state}</strong>!!!
        <br />
        <button onClick={() => setState((acc) => acc + 1)}>Click me</button>
      </p>
    </React.Fragment>
  )
}

Into this:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const React = require("react");
const ReactRefreshRuntime_1 = require(6);
const s_1 = ReactRefreshRuntime_1.createSignatureFunctionForTransform();
function App() {
    s_1();
    const [state, setState] = React.useState(0);
    return (React.createElement(React.Fragment, null,
        React.createElement("p", null,
            "Click Count !!!",
            React.createElement("strong", null, state),
            "!!!",
            React.createElement("br", null),
            React.createElement("button", { onClick: () => setState((acc) => acc + 1) }, "Click me"))));
}
exports.App = App;
s_1(App, "useState");
ReactRefreshRuntime_1.register(App, "./App App");
module.hot.reload();
globalThis.__hot_enqueueUpdate && globalThis.__hot_enqueueUpdate();

Note the visitor is incomplete. It only deals with the most basic use case.

I'm a bit confused by the use of window. It doesn't look to me as if it is really necessary because the implementation is swapped out? What's the point of that?

@leidegre

I think the implementation in Metro does not use window but rather the actual global scope.

I do not know about the original rationale about this implementation, but it has been useful from my experience - it ensures the actual require logic is independent of the fast refresh logic (which means react-refresh/babel transforms can be used with virtually any bundler). As with the swapping, it also acts as a guard to ensure modules that are not supposed to be processed by the runtime will not be processed:

Consider a case where @babel/runtime is being used, which will inject helpers as imports to the bundle and you only want to HMR non node_modules code. If you do not first initialize empty helpers and yet assign helpers to the global scope, a rare case might happen where the Babel-injected helpers will call cleanup before the user-land module actually finishes initialization (because they are child imports).

Is there a guide to implement it without babel?

So two years down the road, the isLikelyComponentType did end up causing confusing "hot module not working" issues for end users, just like @theKashey said 😄 Good someone was able to dig up this old thread when researching a Vite HMR issue related to this vitejs/vite#4583 (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 define call in an IIFE which creates a local $RefreshReg$ and $RefreshSig$, and at the end of the define function body, I'm calling runtime.register with the exports local variable. (Incidentally, I'm not clear how these two $ functions are used, I couldn't see invocations of them anywhere).

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 performReactRefresh.

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 hot.invalidate if not, bubbling to the parent and calling performReactRefresh once we hit a boundary.

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 performReactRefresh function?

commented

(Incidentally, I'm not clear how these two $ functions are used, I couldn't see invocations of them anywhere).

The calls are generated by the react-refresh/babel Babel plugin. Do you have it applied? Without it, nothing else will work.

commented

I could hot-reload the parent so that the module source is re-executed, but that destroys state.

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.
Can you please help me fill some holes to provide a direction for the users still using it in order to migrate to FastRefresh with less friction? 👉 gaearon/react-hot-loader#1848

RHL still has 1M weekly downloads, which is hopefully 1/7th of react-refresh-webpack-plugin, but that is quite a lot in any case.

Sadly, I am not an JS expert. Trying to migrate away from react-hot-loader.

import { AppContainer } from "react-hot-loader";
ReactDOM.render(
    <AppContainer>

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.