facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
228.71k stars 46.81k forks source link

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

Closed shirakaba closed 4 years ago

shirakaba commented 5 years ago

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:

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?

pmmmwh commented 4 years ago

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

IronSean commented 4 years ago

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.

gaearon commented 4 years ago

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

pmmmwh commented 4 years ago

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.

IronSean commented 4 years ago

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

garrett-hopper commented 4 years ago

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

gaearon commented 4 years ago

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

drather19 commented 4 years ago

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 https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/25! (<-- resolved!)

esetnik commented 4 years ago

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

sep2 commented 4 years ago

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),
    },
}
0xMarkian commented 4 years ago

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

gaearon commented 4 years ago

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

JieChang commented 4 years ago

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?

theKashey commented 4 years ago
JieChang commented 4 years ago

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.

JieChang commented 4 years ago

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

theKashey commented 4 years ago
codepunkt commented 4 years ago

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.

silkfire commented 4 years ago

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?

gaearon commented 4 years ago

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.

gaearon commented 4 years ago

I'm going to close this.

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

pmmmwh commented 4 years ago

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.

theKashey commented 4 years ago

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.

amcsi commented 4 years ago

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

gaearon commented 4 years ago

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.

jwchang0206 commented 4 years ago

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

theKashey commented 4 years ago

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

jwchang0206 commented 4 years ago

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?

nwoltman commented 4 years ago

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

theKashey commented 4 years ago

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.

GuillaumeCisco commented 4 years ago

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

leidegre commented 4 years ago

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

shirakaba commented 4 years ago

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

leidegre commented 4 years ago

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.

pmmmwh commented 4 years ago

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

indooorsman commented 3 years ago

Is there a guide to implement it without babel?

fatso83 commented 2 years ago

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 https://github.com/vitejs/vite/discussions/4583#discussioncomment-1797478

giles-v commented 2 years ago

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

gaearon commented 2 years ago

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

gaearon commented 2 years ago

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.

theKashey commented 2 years ago

@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? 👉 https://github.com/gaearon/react-hot-loader/pull/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.

geoHeil commented 2 years ago

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 ?

theKashey commented 2 years ago

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.